release: v2.0.5 - Telegram Business Reporting & Core Resilience

This commit is contained in:
g1nation
2026-05-13 23:54:34 +09:00
parent 6784e85b7e
commit 39386f90b5
12 changed files with 288 additions and 20 deletions
+79 -4
View File
@@ -50,7 +50,8 @@ import {
writeReport,
writeSessionJson,
} from './sessionStore';
import { AgentTurnOutput, CompanyTaskPlan, SessionResult } from './types';
import { buildTelegramReporter, formatCompanyTelegramReport } from './telegramReport';
import { AgentTurnOutput, CompanyState, CompanyTaskPlan, SessionResult } from './types';
/** Trim length applied when an agent's output is fed into the next agent. */
const PEER_OUTPUT_BUDGET = 1500;
@@ -68,6 +69,13 @@ export type CompanyTurnEvent =
| { phase: 'agent-done'; agentId: string; output: AgentTurnOutput; index: number; total: number }
| { phase: 'report-start' }
| { phase: 'report-done'; report: string; ok: boolean }
/**
* Emitted after the secretary attempts to mirror the CEO report to
* Telegram. `ok=true` ⇒ delivered. `ok=false` ⇒ delivery failed (network
* / API error). `ok=null` ⇒ no mirror was attempted because the user
* hasn't opted in (no token, no chat id, or `telegram.enabled=false`).
*/
| { phase: 'telegram-mirror'; ok: boolean | null; reason?: string }
| { phase: 'session-saved'; sessionDir: string }
| { phase: 'aborted'; reason: string };
@@ -78,6 +86,15 @@ export interface DispatcherDeps {
ai: IAIService;
/** Default model to fall back to when an agent has no override. */
defaultModel: string;
/**
* Apply ConnectAI's action-tag executor to the specialist's raw response.
* Without this hook, agent outputs containing `<create_file>` etc. would
* be shown to the user as a *claim* of file creation but nothing would
* actually land on disk. We thread it through deps (rather than
* importing AgentExecutor directly) so the dispatcher stays free of
* sidebar / agent.ts dependencies for unit testability.
*/
executeActionTags?: (text: string) => Promise<string[]>;
/** Per-call cancellation. The sidebar's Stop button flips this. */
signal?: AbortSignal;
/** Optional event sink for the webview. Receives events synchronously. */
@@ -160,6 +177,35 @@ export async function runCompanyTurn(
writeReport(sessionDir, reportResult.report);
emit({ phase: 'report-done', report: reportResult.report, ok: reportResult.ok });
// ── Phase 3.5: Secretary mirror to Telegram ──
// Origin parity (Connect_origin extension.ts ~line 20620): after the CEO
// synthesizes the round, the Secretary (영숙) ships a digest to the user's
// Telegram chat. Strictly best-effort — failures never break the turn.
let telegramOk: boolean | null = null;
let telegramReason: string | undefined;
try {
const reporter = await buildTelegramReporter(deps.context);
if (!reporter) {
telegramReason = 'no-config'; // opt-in off, no token, or no chat id
} else {
const tgText = formatCompanyTelegramReport({
state,
userPrompt,
plan: plannerResult.plan,
outputs,
report: reportResult.report,
sessionTimestamp: timestamp,
});
telegramOk = await reporter(tgText);
if (!telegramOk) telegramReason = 'delivery-failed';
}
} catch (e: any) {
telegramOk = false;
telegramReason = e?.message ?? String(e);
logError('company.dispatcher: telegram mirror threw.', { error: telegramReason });
}
emit({ phase: 'telegram-mirror', ok: telegramOk, reason: telegramReason });
// ── Phase 4: persist + side effects ──
const result: SessionResult = {
timestamp, sessionDir,
@@ -232,12 +278,31 @@ async function _dispatchOne(
user: task,
model,
});
const response = (result.content || '').trim();
const rawResponse = (result.content || '').trim();
// Apply ConnectAI's action-tag executor so `<create_file>`,
// `<run_command>`, `<edit_file>`, etc. emitted by the agent actually
// hit disk / shell. The report (e.g. "✅ Created: foo.py") is
// appended to the response so the user sees what really happened.
let finalResponse = rawResponse || '_(empty response)_';
if (rawResponse && deps.executeActionTags && _hasActionTag(rawResponse)) {
try {
const report = await deps.executeActionTags(rawResponse);
if (report.length > 0) {
finalResponse = `${rawResponse}\n\n---\n**Action 실행 결과:**\n${report.map((r) => `- ${r}`).join('\n')}`;
}
} catch (e: any) {
// Surface the failure but keep the agent's text — partial
// success is more useful than dropping the whole response.
const err = e?.message ?? String(e);
logError('company.dispatcher: action-tag execution failed.', { agentId, err });
finalResponse = `${rawResponse}\n\n---\n⚠️ Action 실행 실패: ${err}`;
}
}
return {
agentId, task,
response: response || '_(empty response)_',
response: finalResponse,
durationMs: Date.now() - startedAt,
error: response ? undefined : 'empty-response',
error: rawResponse ? undefined : 'empty-response',
};
} catch (e: any) {
const err = e?.message ?? String(e);
@@ -250,3 +315,13 @@ async function _dispatchOne(
};
}
}
/**
* Cheap pre-check so we don't fire up the action-tag executor for every
* specialist response — only the ones that actually contain a recognised
* tag. Saves a workspace lookup + transaction-manager spin-up on the common
* case (the agent just talks).
*/
function _hasActionTag(text: string): boolean {
return /<\s*(?:create_file|edit_file|delete_file|read_file|list_files|list_brain|run_command|read_brain|reveal_in_explorer|open_file|glob|grep)\b/i.test(text);
}