feat: plan 18
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Body, Controller, Get, Headers, Inject, Post, Query } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Headers, Inject, Post, Query, Res } from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
import { ok } from '../common/api-response.js';
|
||||
import { actorFromHeaders } from '../common/auth-context.js';
|
||||
import { ChatAskInput, ChatService } from './chat.service.js';
|
||||
@@ -12,6 +13,15 @@ export class ChatController {
|
||||
return ok(this.svc.ask(body, actorFromHeaders(headers)));
|
||||
}
|
||||
|
||||
@Post('ask/stream')
|
||||
askStream(
|
||||
@Headers() headers: Record<string, string | string[] | undefined>,
|
||||
@Body() body: ChatAskInput,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.svc.streamAsk(body, actorFromHeaders(headers), res);
|
||||
}
|
||||
|
||||
@Get('audit/verify')
|
||||
verifyAudit() {
|
||||
return ok(this.svc.verifyAudit());
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ForbiddenException, Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { execFileSync, spawn } from 'node:child_process';
|
||||
import { join } from 'node:path';
|
||||
import type { Response } from 'express';
|
||||
import { APP_ROOT } from '../common/app-root.js';
|
||||
import type { SettingsActor } from '../settings/settings.service.js';
|
||||
|
||||
@@ -94,6 +95,49 @@ export class ChatService {
|
||||
return { ok: res.status === 0, output: res.stdout || res.stderr };
|
||||
}
|
||||
|
||||
/**
|
||||
* Item 3: streaming read-only/analysis turns. The harness emits two NDJSON
|
||||
* phases — an UNCERTIFIED deterministic draft, then the certified final. We
|
||||
* only wrap the harness; RBAC/H4/router verdicts remain harness-owned. The
|
||||
* spawn is read-only (no side effect), so it is safe to stream the draft.
|
||||
*/
|
||||
streamAsk(input: ChatAskInput, actor: SettingsActor, res: Response) {
|
||||
if (!input.message || !input.message.trim()) {
|
||||
throw new ForbiddenException('CHAT_DENY message required');
|
||||
}
|
||||
this.requireRead(actor);
|
||||
const args = [
|
||||
CHAT_CLI,
|
||||
'ask',
|
||||
'--stream',
|
||||
'--message',
|
||||
input.message,
|
||||
'--actor',
|
||||
actor.actor,
|
||||
'--role',
|
||||
actor.role,
|
||||
'--project',
|
||||
actor.project,
|
||||
'--chat-id',
|
||||
input.chatId || 'chat-default',
|
||||
'--tenant',
|
||||
actor.tenant,
|
||||
];
|
||||
if (input.agentId) args.push('--agent', input.agentId);
|
||||
if (input.skillId) args.push('--skill', input.skillId);
|
||||
if (input.delegationLevel !== undefined) args.push('--delegation-level', String(input.delegationLevel));
|
||||
res.setHeader('Content-Type', 'application/x-ndjson; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
const child = spawn('python3', args, { cwd: APP_ROOT, env: { ...process.env } });
|
||||
child.stdout.on('data', (chunk) => res.write(chunk));
|
||||
child.on('error', () => {
|
||||
if (!res.headersSent) res.status(500);
|
||||
res.end();
|
||||
});
|
||||
child.on('close', () => res.end());
|
||||
}
|
||||
|
||||
replay(chatId = '', turnId = '', tenant = '') {
|
||||
const args = ['replay'];
|
||||
if (chatId) args.push('--chat-id', chatId);
|
||||
|
||||
Reference in New Issue
Block a user