tycono-server 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +35 -0
- package/bin/server.ts +160 -0
- package/package.json +50 -0
- package/src/api/package.json +31 -0
- package/src/api/src/create-app.ts +90 -0
- package/src/api/src/create-server.ts +251 -0
- package/src/api/src/engine/agent-loop.ts +738 -0
- package/src/api/src/engine/authority-validator.ts +149 -0
- package/src/api/src/engine/context-assembler.ts +912 -0
- package/src/api/src/engine/index.ts +27 -0
- package/src/api/src/engine/knowledge-gate.ts +365 -0
- package/src/api/src/engine/llm-adapter.ts +304 -0
- package/src/api/src/engine/org-tree.ts +270 -0
- package/src/api/src/engine/role-lifecycle.ts +369 -0
- package/src/api/src/engine/runners/claude-cli.ts +796 -0
- package/src/api/src/engine/runners/direct-api.ts +66 -0
- package/src/api/src/engine/runners/index.ts +30 -0
- package/src/api/src/engine/runners/types.ts +95 -0
- package/src/api/src/engine/skill-template.ts +134 -0
- package/src/api/src/engine/tools/definitions.ts +201 -0
- package/src/api/src/engine/tools/executor.ts +611 -0
- package/src/api/src/routes/active-sessions.ts +134 -0
- package/src/api/src/routes/coins.ts +153 -0
- package/src/api/src/routes/company.ts +57 -0
- package/src/api/src/routes/cost.ts +141 -0
- package/src/api/src/routes/engine.ts +220 -0
- package/src/api/src/routes/execute.ts +1075 -0
- package/src/api/src/routes/git.ts +211 -0
- package/src/api/src/routes/knowledge.ts +378 -0
- package/src/api/src/routes/operations.ts +309 -0
- package/src/api/src/routes/preferences.ts +63 -0
- package/src/api/src/routes/presets.ts +123 -0
- package/src/api/src/routes/projects.ts +82 -0
- package/src/api/src/routes/quests.ts +41 -0
- package/src/api/src/routes/roles.ts +112 -0
- package/src/api/src/routes/save.ts +152 -0
- package/src/api/src/routes/sessions.ts +288 -0
- package/src/api/src/routes/setup.ts +437 -0
- package/src/api/src/routes/skills.ts +357 -0
- package/src/api/src/routes/speech.ts +959 -0
- package/src/api/src/routes/supervision.ts +136 -0
- package/src/api/src/routes/sync.ts +165 -0
- package/src/api/src/server.ts +59 -0
- package/src/api/src/services/activity-stream.ts +184 -0
- package/src/api/src/services/activity-tracker.ts +115 -0
- package/src/api/src/services/claude-md-manager.ts +94 -0
- package/src/api/src/services/company-config.ts +115 -0
- package/src/api/src/services/database.ts +77 -0
- package/src/api/src/services/digest-engine.ts +313 -0
- package/src/api/src/services/execution-manager.ts +1036 -0
- package/src/api/src/services/file-reader.ts +77 -0
- package/src/api/src/services/git-save.ts +614 -0
- package/src/api/src/services/job-manager.ts +16 -0
- package/src/api/src/services/knowledge-importer.ts +466 -0
- package/src/api/src/services/markdown-parser.ts +173 -0
- package/src/api/src/services/port-registry.ts +222 -0
- package/src/api/src/services/preferences.ts +150 -0
- package/src/api/src/services/preset-loader.ts +149 -0
- package/src/api/src/services/pricing.ts +34 -0
- package/src/api/src/services/scaffold.ts +546 -0
- package/src/api/src/services/session-store.ts +340 -0
- package/src/api/src/services/supervisor-heartbeat.ts +897 -0
- package/src/api/src/services/team-recommender.ts +382 -0
- package/src/api/src/services/token-ledger.ts +127 -0
- package/src/api/src/services/wave-messages.ts +194 -0
- package/src/api/src/services/wave-multiplexer.ts +356 -0
- package/src/api/src/services/wave-tracker.ts +359 -0
- package/src/api/src/utils/role-level.ts +31 -0
- package/src/core/scaffolder.ts +620 -0
- package/src/shared/types.ts +224 -0
- package/templates/CLAUDE.md.tmpl +239 -0
- package/templates/company.md.tmpl +17 -0
- package/templates/gitignore.tmpl +28 -0
- package/templates/roles.md.tmpl +8 -0
- package/templates/skills/_manifest.json +23 -0
- package/templates/skills/agent-browser/SKILL.md +159 -0
- package/templates/skills/agent-browser/meta.json +19 -0
- package/templates/skills/akb-linter/SKILL.md +125 -0
- package/templates/skills/akb-linter/meta.json +12 -0
- package/templates/skills/knowledge-gate/SKILL.md +120 -0
- package/templates/skills/knowledge-gate/meta.json +12 -0
- package/templates/teams/agency.json +58 -0
- package/templates/teams/research.json +58 -0
- package/templates/teams/startup.json +58 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
import { AnthropicProvider, type LLMProvider, type LLMMessage, type ToolResult, type MessageContent } from './llm-adapter.js';
|
|
2
|
+
import { type OrgTree, getSubordinates } from './org-tree.js';
|
|
3
|
+
import { assembleContext, type TeamStatus } from './context-assembler.js';
|
|
4
|
+
import { validateDispatch, validateConsult } from './authority-validator.js';
|
|
5
|
+
import { getToolsForRole } from './tools/definitions.js';
|
|
6
|
+
import { executeTool, type ToolExecutorOptions } from './tools/executor.js';
|
|
7
|
+
import { type TokenLedger } from '../services/token-ledger.js';
|
|
8
|
+
import { estimateCost } from '../services/pricing.js';
|
|
9
|
+
import { type ImageAttachment } from './runners/types.js';
|
|
10
|
+
|
|
11
|
+
/* ─── Types ──────────────────────────────────── */
|
|
12
|
+
|
|
13
|
+
export interface AgentConfig {
|
|
14
|
+
companyRoot: string;
|
|
15
|
+
roleId: string;
|
|
16
|
+
task: string;
|
|
17
|
+
sourceRole: string;
|
|
18
|
+
orgTree: OrgTree;
|
|
19
|
+
readOnly?: boolean;
|
|
20
|
+
maxTurns?: number;
|
|
21
|
+
codeRoot?: string; // EG-001: code project root for bash_execute
|
|
22
|
+
llm?: LLMProvider;
|
|
23
|
+
depth?: number; // Current dispatch depth (default 0)
|
|
24
|
+
visitedRoles?: Set<string>; // Circular dispatch detection
|
|
25
|
+
abortSignal?: AbortSignal; // Abort signal for cancellation
|
|
26
|
+
teamStatus?: TeamStatus; // Current team member statuses
|
|
27
|
+
sessionId: string; // D-014: Session ID for token tracking (required)
|
|
28
|
+
model?: string; // LLM model name for cost tracking
|
|
29
|
+
tokenLedger?: TokenLedger; // Token usage ledger (optional)
|
|
30
|
+
attachments?: ImageAttachment[]; // Image attachments for vision
|
|
31
|
+
targetRoles?: string[]; // Selective dispatch scope
|
|
32
|
+
presetId?: string; // Wave-scoped preset for knowledge injection
|
|
33
|
+
// Callbacks
|
|
34
|
+
onText?: (text: string) => void;
|
|
35
|
+
onToolExec?: (name: string, input: Record<string, unknown>) => void;
|
|
36
|
+
onDispatch?: (roleId: string, task: string) => void;
|
|
37
|
+
onConsult?: (roleId: string, question: string) => void;
|
|
38
|
+
onTurnComplete?: (turn: number) => void;
|
|
39
|
+
/** Trace: emitted when system prompt is assembled */
|
|
40
|
+
onPromptAssembled?: (systemPrompt: string, userTask: string) => void;
|
|
41
|
+
/** Supervision: abort a running session */
|
|
42
|
+
onAbortSession?: (sessionId: string) => boolean;
|
|
43
|
+
/** Supervision: amend a running session with new instructions */
|
|
44
|
+
onAmendSession?: (sessionId: string, instruction: string) => boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AgentResult {
|
|
48
|
+
output: string;
|
|
49
|
+
turns: number;
|
|
50
|
+
totalTokens: { input: number; output: number };
|
|
51
|
+
toolCalls: Array<{ name: string; input: Record<string, unknown> }>;
|
|
52
|
+
dispatches: Array<{ roleId: string; task: string; result: string }>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ─── EG-006: Context Compression ────────────── */
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Compress older messages to reduce token usage.
|
|
59
|
+
*
|
|
60
|
+
* SV-9 Enhancement: Zone-based compression for supervision sessions.
|
|
61
|
+
* - Zone A (pinned): first 2 messages (system prompt + original task + plan) — never compress
|
|
62
|
+
* - Zone B (rolling): middle messages — heartbeat ticks get aggressive compression
|
|
63
|
+
* - Zone C (recent): last 4 messages — preserve for LLM context
|
|
64
|
+
*
|
|
65
|
+
* Heartbeat-specific: consecutive quiet ticks are merged into a single line.
|
|
66
|
+
*/
|
|
67
|
+
function compressMessages(messages: LLMMessage[]): void {
|
|
68
|
+
if (messages.length <= 6) return;
|
|
69
|
+
|
|
70
|
+
// Zone A: first 2, Zone C: last 4
|
|
71
|
+
const keepHead = 2;
|
|
72
|
+
const keepTail = 4;
|
|
73
|
+
const compressRange = messages.slice(keepHead, messages.length - keepTail);
|
|
74
|
+
|
|
75
|
+
// Track consecutive quiet heartbeat ticks for merging
|
|
76
|
+
let quietTickStart = -1;
|
|
77
|
+
let quietTickCount = 0;
|
|
78
|
+
|
|
79
|
+
for (let idx = 0; idx < compressRange.length; idx++) {
|
|
80
|
+
const msg = compressRange[idx];
|
|
81
|
+
|
|
82
|
+
if (typeof msg.content === 'string') {
|
|
83
|
+
// Check if this is a heartbeat quiet tick result
|
|
84
|
+
const isQuietTick = msg.content.includes('sessions progressing normally') && msg.content.includes('No anomalies');
|
|
85
|
+
|
|
86
|
+
if (isQuietTick) {
|
|
87
|
+
quietTickCount++;
|
|
88
|
+
if (quietTickStart === -1) quietTickStart = idx;
|
|
89
|
+
|
|
90
|
+
// Merge consecutive quiet ticks
|
|
91
|
+
if (quietTickCount > 1) {
|
|
92
|
+
msg.content = `[Quiet ticks merged: ${quietTickCount} ticks, no anomalies]`;
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Reset quiet tick counter on non-quiet content
|
|
98
|
+
quietTickStart = -1;
|
|
99
|
+
quietTickCount = 0;
|
|
100
|
+
|
|
101
|
+
// Truncate long text content
|
|
102
|
+
if (msg.content.length > 500) {
|
|
103
|
+
msg.content = msg.content.slice(0, 300) + '\n\n[... compressed ...]';
|
|
104
|
+
}
|
|
105
|
+
} else if (Array.isArray(msg.content)) {
|
|
106
|
+
for (let i = 0; i < msg.content.length; i++) {
|
|
107
|
+
const block = msg.content[i] as Record<string, unknown>;
|
|
108
|
+
if (block.type === 'tool_result') {
|
|
109
|
+
const content = typeof block.content === 'string' ? block.content : '';
|
|
110
|
+
|
|
111
|
+
// Heartbeat digest results: compress more aggressively
|
|
112
|
+
const isDigest = content.includes('Supervision Digest') || content.includes('sessions progressing normally');
|
|
113
|
+
const maxLen = isDigest ? 150 : 300;
|
|
114
|
+
|
|
115
|
+
if (content.length > maxLen) {
|
|
116
|
+
block.content = content.slice(0, maxLen - 50) + '\n[... compressed, was ' + content.length + ' chars]';
|
|
117
|
+
}
|
|
118
|
+
} else if (block.type === 'text' && typeof block.text === 'string' && block.text.length > 500) {
|
|
119
|
+
block.text = (block.text as string).slice(0, 300) + '\n[... compressed ...]';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ─── Agent Loop ─────────────────────────────── */
|
|
127
|
+
|
|
128
|
+
export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
|
|
129
|
+
const {
|
|
130
|
+
companyRoot,
|
|
131
|
+
roleId,
|
|
132
|
+
task,
|
|
133
|
+
sourceRole,
|
|
134
|
+
orgTree,
|
|
135
|
+
readOnly = false,
|
|
136
|
+
maxTurns = 20,
|
|
137
|
+
abortSignal,
|
|
138
|
+
onText,
|
|
139
|
+
onToolExec,
|
|
140
|
+
onDispatch: onDispatchCallback,
|
|
141
|
+
onConsult: onConsultCallback,
|
|
142
|
+
onTurnComplete,
|
|
143
|
+
} = config;
|
|
144
|
+
|
|
145
|
+
// Depth and circular dispatch guard
|
|
146
|
+
const depth = config.depth ?? 0;
|
|
147
|
+
const visitedRoles = config.visitedRoles ?? new Set<string>();
|
|
148
|
+
|
|
149
|
+
// Depth limit check
|
|
150
|
+
if (depth >= 3) {
|
|
151
|
+
return {
|
|
152
|
+
output: `[DISPATCH BLOCKED] Max dispatch depth (3) exceeded. Role: ${roleId}`,
|
|
153
|
+
turns: 0,
|
|
154
|
+
totalTokens: { input: 0, output: 0 },
|
|
155
|
+
toolCalls: [],
|
|
156
|
+
dispatches: [],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Mark current role as visited
|
|
161
|
+
visitedRoles.add(roleId);
|
|
162
|
+
|
|
163
|
+
const llm = config.llm ?? new AnthropicProvider();
|
|
164
|
+
|
|
165
|
+
// 1. Assemble context
|
|
166
|
+
const context = assembleContext(companyRoot, roleId, task, sourceRole, orgTree, { teamStatus: config.teamStatus, targetRoles: config.targetRoles, presetId: config.presetId });
|
|
167
|
+
|
|
168
|
+
// Trace: capture assembled prompt for debugging
|
|
169
|
+
config.onPromptAssembled?.(context.systemPrompt, task);
|
|
170
|
+
|
|
171
|
+
// 2. Determine tools
|
|
172
|
+
const subordinates = getSubordinates(orgTree, roleId);
|
|
173
|
+
const hasBash = !readOnly && !!config.codeRoot;
|
|
174
|
+
const node = orgTree.nodes.get(roleId);
|
|
175
|
+
const heartbeatEnabled = node?.heartbeat?.enabled === true && subordinates.length > 0;
|
|
176
|
+
// Peers = other roles with the same reportsTo (same parent in org tree)
|
|
177
|
+
const parentId = node?.reportsTo;
|
|
178
|
+
const hasPeers = parentId
|
|
179
|
+
? (getSubordinates(orgTree, parentId).filter(id => id !== roleId).length > 0)
|
|
180
|
+
: false;
|
|
181
|
+
const tools = getToolsForRole(subordinates.length > 0, readOnly, hasBash, heartbeatEnabled, hasPeers);
|
|
182
|
+
|
|
183
|
+
// 3. Set up tool executor
|
|
184
|
+
const toolExecOptions: ToolExecutorOptions = {
|
|
185
|
+
companyRoot,
|
|
186
|
+
roleId,
|
|
187
|
+
orgTree,
|
|
188
|
+
codeRoot: config.codeRoot,
|
|
189
|
+
sessionId: config.sessionId,
|
|
190
|
+
onToolExec,
|
|
191
|
+
onAbortSession: config.onAbortSession,
|
|
192
|
+
onAmendSession: config.onAmendSession,
|
|
193
|
+
onDispatch: async (targetRoleId: string, subTask: string) => {
|
|
194
|
+
// Recursive dispatch — validate, then run sub-agent
|
|
195
|
+
const authResult = validateDispatch(orgTree, roleId, targetRoleId);
|
|
196
|
+
if (!authResult.allowed) {
|
|
197
|
+
return `Dispatch rejected: ${authResult.reason}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Circular dispatch detection
|
|
201
|
+
if (visitedRoles.has(targetRoleId)) {
|
|
202
|
+
return `[DISPATCH BLOCKED] Circular dispatch detected: ${roleId} → ${targetRoleId}. Chain: ${[...visitedRoles].join(' → ')}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
onDispatchCallback?.(targetRoleId, subTask);
|
|
206
|
+
|
|
207
|
+
// Run sub-agent (recursive) — pass depth+1 and a copy of visitedRoles
|
|
208
|
+
const subResult = await runAgentLoop({
|
|
209
|
+
companyRoot,
|
|
210
|
+
roleId: targetRoleId,
|
|
211
|
+
task: subTask,
|
|
212
|
+
sourceRole: roleId,
|
|
213
|
+
orgTree,
|
|
214
|
+
readOnly: false,
|
|
215
|
+
maxTurns: Math.min(maxTurns, 15), // Limit sub-agent turns
|
|
216
|
+
codeRoot: config.codeRoot,
|
|
217
|
+
llm,
|
|
218
|
+
depth: depth + 1,
|
|
219
|
+
visitedRoles: new Set(visitedRoles), // Copy for parallel dispatch support
|
|
220
|
+
abortSignal,
|
|
221
|
+
sessionId: config.sessionId,
|
|
222
|
+
model: config.model,
|
|
223
|
+
tokenLedger: config.tokenLedger,
|
|
224
|
+
onText: (text) => onText?.(`[${targetRoleId}] ${text}`),
|
|
225
|
+
onToolExec,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Aggregate sub-agent tokens into parent totals
|
|
229
|
+
totalInput += subResult.totalTokens.input;
|
|
230
|
+
totalOutput += subResult.totalTokens.output;
|
|
231
|
+
|
|
232
|
+
return subResult.output;
|
|
233
|
+
},
|
|
234
|
+
onConsult: async (targetRoleId: string, question: string) => {
|
|
235
|
+
// Authority check
|
|
236
|
+
const authResult = validateConsult(orgTree, roleId, targetRoleId);
|
|
237
|
+
if (!authResult.allowed) {
|
|
238
|
+
return `Consult rejected: ${authResult.reason}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Circular consult detection
|
|
242
|
+
if (visitedRoles.has(targetRoleId)) {
|
|
243
|
+
return `[CONSULT BLOCKED] Circular consult detected: ${roleId} → ${targetRoleId}. Chain: ${[...visitedRoles].join(' → ')}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
onConsultCallback?.(targetRoleId, question);
|
|
247
|
+
|
|
248
|
+
// Run sub-agent in read-only mode for the consulted role
|
|
249
|
+
const consultTask = `[Consultation from ${roleId}] ${question}\n\nAnswer this question based on your role's expertise and knowledge. Be concise and specific.`;
|
|
250
|
+
const subResult = await runAgentLoop({
|
|
251
|
+
companyRoot,
|
|
252
|
+
roleId: targetRoleId,
|
|
253
|
+
task: consultTask,
|
|
254
|
+
sourceRole: roleId,
|
|
255
|
+
orgTree,
|
|
256
|
+
readOnly: true, // Consult is always read-only
|
|
257
|
+
maxTurns: Math.min(maxTurns, 10), // Limit consult turns
|
|
258
|
+
llm,
|
|
259
|
+
depth: depth + 1,
|
|
260
|
+
visitedRoles: new Set(visitedRoles),
|
|
261
|
+
abortSignal,
|
|
262
|
+
sessionId: config.sessionId,
|
|
263
|
+
model: config.model,
|
|
264
|
+
tokenLedger: config.tokenLedger,
|
|
265
|
+
onText: (text) => onText?.(`[consult:${targetRoleId}] ${text}`),
|
|
266
|
+
onToolExec,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Aggregate sub-agent tokens
|
|
270
|
+
totalInput += subResult.totalTokens.input;
|
|
271
|
+
totalOutput += subResult.totalTokens.output;
|
|
272
|
+
|
|
273
|
+
return subResult.output;
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// 4. Run the loop
|
|
278
|
+
// Build initial user message with optional image attachments
|
|
279
|
+
const userContent: MessageContent[] = [];
|
|
280
|
+
|
|
281
|
+
// Add image attachments first (if any)
|
|
282
|
+
if (config.attachments && config.attachments.length > 0) {
|
|
283
|
+
for (const att of config.attachments) {
|
|
284
|
+
userContent.push({
|
|
285
|
+
type: 'image',
|
|
286
|
+
source: {
|
|
287
|
+
type: 'base64',
|
|
288
|
+
media_type: att.mediaType,
|
|
289
|
+
data: att.data,
|
|
290
|
+
},
|
|
291
|
+
} as unknown as MessageContent);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Add text content
|
|
296
|
+
userContent.push({ type: 'text', text: task });
|
|
297
|
+
|
|
298
|
+
const messages: LLMMessage[] = [
|
|
299
|
+
{ role: 'user', content: userContent.length === 1 ? task : userContent },
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
let turns = 0;
|
|
303
|
+
let totalInput = 0;
|
|
304
|
+
let totalOutput = 0;
|
|
305
|
+
const allToolCalls: AgentResult['toolCalls'] = [];
|
|
306
|
+
const dispatches: AgentResult['dispatches'] = [];
|
|
307
|
+
const outputParts: string[] = [];
|
|
308
|
+
|
|
309
|
+
// EG-006/007: Context compression + token budget
|
|
310
|
+
const COMPRESS_THRESHOLD = 100_000;
|
|
311
|
+
const TOKEN_WARN_THRESHOLD = 200_000; // Warn at 200K total tokens
|
|
312
|
+
let tokenWarningEmitted = false;
|
|
313
|
+
|
|
314
|
+
while (turns < maxTurns) {
|
|
315
|
+
// Check abort signal before each turn
|
|
316
|
+
if (abortSignal?.aborted) break;
|
|
317
|
+
|
|
318
|
+
turns++;
|
|
319
|
+
|
|
320
|
+
// EG-006: Compress old messages when token budget exceeded
|
|
321
|
+
if (totalInput > COMPRESS_THRESHOLD && messages.length > 4) {
|
|
322
|
+
compressMessages(messages);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Call LLM
|
|
326
|
+
const response = await llm.chat(context.systemPrompt, messages, tools, abortSignal);
|
|
327
|
+
totalInput += response.usage.inputTokens;
|
|
328
|
+
totalOutput += response.usage.outputTokens;
|
|
329
|
+
|
|
330
|
+
// EG-007: Token budget warning
|
|
331
|
+
if (!tokenWarningEmitted && (totalInput + totalOutput) > TOKEN_WARN_THRESHOLD) {
|
|
332
|
+
tokenWarningEmitted = true;
|
|
333
|
+
const cost = estimateCost(totalInput, totalOutput, config.model ?? 'unknown');
|
|
334
|
+
onText?.(`\n\n⚠️ [Token Budget Warning] This task has used ${totalInput.toLocaleString()} input + ${totalOutput.toLocaleString()} output tokens (~$${cost.toFixed(3)}). Consider wrapping up.\n\n`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Record token usage
|
|
338
|
+
config.tokenLedger?.record({
|
|
339
|
+
ts: new Date().toISOString(),
|
|
340
|
+
sessionId: config.sessionId,
|
|
341
|
+
roleId,
|
|
342
|
+
model: config.model ?? 'unknown',
|
|
343
|
+
inputTokens: response.usage.inputTokens,
|
|
344
|
+
outputTokens: response.usage.outputTokens,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Process response content
|
|
348
|
+
const assistantContent: MessageContent[] = response.content;
|
|
349
|
+
messages.push({ role: 'assistant', content: assistantContent });
|
|
350
|
+
|
|
351
|
+
// Extract text parts
|
|
352
|
+
for (const block of response.content) {
|
|
353
|
+
if (block.type === 'text' && block.text) {
|
|
354
|
+
outputParts.push(block.text);
|
|
355
|
+
onText?.(block.text);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// If no tool use, we're done
|
|
360
|
+
if (response.stopReason === 'end_turn' || response.stopReason !== 'tool_use') {
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Process tool calls
|
|
365
|
+
const toolCalls = response.content.filter(
|
|
366
|
+
(b): b is MessageContent & { type: 'tool_use' } => b.type === 'tool_use'
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
// EG-004: Parallel tool execution for independent tools
|
|
370
|
+
// dispatch/consult/heartbeat run sequentially (recursive agent calls / blocking)
|
|
371
|
+
// All other tools run in parallel via Promise.all()
|
|
372
|
+
const sequentialTools = new Set(['dispatch', 'consult', 'heartbeat_watch']);
|
|
373
|
+
const parallelCalls = toolCalls.filter(tc => !sequentialTools.has(tc.name));
|
|
374
|
+
const sequentialCalls = toolCalls.filter(tc => sequentialTools.has(tc.name));
|
|
375
|
+
|
|
376
|
+
// Record all tool calls
|
|
377
|
+
for (const tc of toolCalls) {
|
|
378
|
+
allToolCalls.push({ name: tc.name, input: tc.input });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Run parallel tools concurrently
|
|
382
|
+
const parallelResults = await Promise.all(
|
|
383
|
+
parallelCalls.map(tc =>
|
|
384
|
+
executeTool({ id: tc.id, name: tc.name, input: tc.input }, toolExecOptions)
|
|
385
|
+
)
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// Run sequential tools one by one
|
|
389
|
+
const sequentialResults: ToolResult[] = [];
|
|
390
|
+
for (const tc of sequentialCalls) {
|
|
391
|
+
const result = await executeTool(
|
|
392
|
+
{ id: tc.id, name: tc.name, input: tc.input },
|
|
393
|
+
toolExecOptions,
|
|
394
|
+
);
|
|
395
|
+
sequentialResults.push(result);
|
|
396
|
+
|
|
397
|
+
// Track dispatches
|
|
398
|
+
if (tc.name === 'dispatch' && !result.is_error) {
|
|
399
|
+
dispatches.push({
|
|
400
|
+
roleId: String(tc.input.roleId),
|
|
401
|
+
task: String(tc.input.task),
|
|
402
|
+
result: result.content,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// EG-005: Merge results in original tool_use_id order
|
|
408
|
+
const resultMap = new Map<string, ToolResult>();
|
|
409
|
+
for (const r of [...parallelResults, ...sequentialResults]) {
|
|
410
|
+
resultMap.set(r.tool_use_id, r);
|
|
411
|
+
}
|
|
412
|
+
const toolResults = toolCalls.map(tc => resultMap.get(tc.id)!);
|
|
413
|
+
|
|
414
|
+
// Track dispatches from parallel results too
|
|
415
|
+
for (const tc of parallelCalls) {
|
|
416
|
+
if (tc.name === 'dispatch') {
|
|
417
|
+
const r = resultMap.get(tc.id)!;
|
|
418
|
+
if (!r.is_error) {
|
|
419
|
+
dispatches.push({
|
|
420
|
+
roleId: String(tc.input.roleId),
|
|
421
|
+
task: String(tc.input.task),
|
|
422
|
+
result: r.content,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Send tool results back
|
|
429
|
+
messages.push({
|
|
430
|
+
role: 'user',
|
|
431
|
+
content: toolResults.map((r) => ({
|
|
432
|
+
type: 'tool_result' as const,
|
|
433
|
+
tool_use_id: r.tool_use_id,
|
|
434
|
+
content: r.content,
|
|
435
|
+
is_error: r.is_error,
|
|
436
|
+
})) as unknown as MessageContent[],
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
onTurnComplete?.(turns);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── Post-execution phases (depth 0 only) ──
|
|
443
|
+
if (!readOnly && depth === 0 && turns > 0) {
|
|
444
|
+
const node = orgTree.nodes.get(roleId);
|
|
445
|
+
const isCLevel = node?.level === 'c-level';
|
|
446
|
+
|
|
447
|
+
// Phase A: C-Level Supervision Loop — review dispatches, update knowledge, dispatch next
|
|
448
|
+
if (isCLevel && dispatches.length > 0) {
|
|
449
|
+
const dispatchSummary = dispatches.map((d, i) =>
|
|
450
|
+
`${i + 1}. **${d.roleId}**: "${d.task.slice(0, 80)}"\n Result: ${d.result.slice(0, 300)}`,
|
|
451
|
+
).join('\n\n');
|
|
452
|
+
|
|
453
|
+
// Build list of already-dispatched tasks to prevent re-dispatch
|
|
454
|
+
const dispatchedList = dispatches.map(d => `- ${d.roleId}: "${d.task.slice(0, 100)}"`).join('\n');
|
|
455
|
+
|
|
456
|
+
const supervisionPrompt = [
|
|
457
|
+
'[SUPERVISION LOOP] Your subordinates have completed their tasks. Follow the C-Level Protocol:',
|
|
458
|
+
'',
|
|
459
|
+
'## Subordinate Results',
|
|
460
|
+
dispatchSummary,
|
|
461
|
+
'',
|
|
462
|
+
'## Already Dispatched (DO NOT re-dispatch these)',
|
|
463
|
+
dispatchedList,
|
|
464
|
+
'',
|
|
465
|
+
'⛔ **Do NOT re-dispatch the same or similar task to the same role.** If a subordinate already completed a task, accept the result and move on.',
|
|
466
|
+
'⛔ **If the result is satisfactory, do NOT dispatch again.** Only re-dispatch if the result clearly fails acceptance criteria with SPECIFIC feedback on what to fix.',
|
|
467
|
+
'',
|
|
468
|
+
'## Required Actions (do ALL of these):',
|
|
469
|
+
'',
|
|
470
|
+
'### 1. Review',
|
|
471
|
+
'Does each result meet the acceptance criteria? If clearly unsatisfactory, re-dispatch with SPECIFIC fix instructions (not the same task again).',
|
|
472
|
+
'',
|
|
473
|
+
'### 2. Knowledge Update (The Loop Step ④)',
|
|
474
|
+
'Record any new decisions, findings, or analysis in appropriate AKB documents:',
|
|
475
|
+
'- Update your journal (`roles/' + roleId + '/journal/`)',
|
|
476
|
+
'- Update relevant project docs if needed',
|
|
477
|
+
'- Update knowledge/ if there are reusable insights',
|
|
478
|
+
'',
|
|
479
|
+
'### 3. Task Update (The Loop Step ⑤)',
|
|
480
|
+
'Update task status in the relevant tasks.md or project documents.',
|
|
481
|
+
'Mark completed items as DONE. Identify the NEXT task to dispatch.',
|
|
482
|
+
'',
|
|
483
|
+
'### 4. Next Dispatch (ONLY if there is genuinely NEW work)',
|
|
484
|
+
'If there are DIFFERENT remaining tasks (e.g., QA after Engineer, or a DIFFERENT backlog item):',
|
|
485
|
+
'- Dispatch the NEXT DIFFERENT task to the appropriate subordinate',
|
|
486
|
+
'- If all work from the directive is done, synthesize a final report for your superior',
|
|
487
|
+
'- **If the subordinate already completed the requested work, report success — do NOT re-dispatch**',
|
|
488
|
+
'',
|
|
489
|
+
'Execute these actions now using your tools (Read, Edit, Bash, dispatch).',
|
|
490
|
+
].join('\n');
|
|
491
|
+
|
|
492
|
+
// Run supervision loop (up to 3 additional rounds of tool use)
|
|
493
|
+
messages.push({ role: 'user', content: supervisionPrompt });
|
|
494
|
+
const maxSupervisionRounds = 3;
|
|
495
|
+
for (let round = 0; round < maxSupervisionRounds && turns < maxTurns; round++) {
|
|
496
|
+
if (abortSignal?.aborted) break;
|
|
497
|
+
turns++;
|
|
498
|
+
|
|
499
|
+
const supResponse = await llm.chat(context.systemPrompt, messages, tools, abortSignal);
|
|
500
|
+
totalInput += supResponse.usage.inputTokens;
|
|
501
|
+
totalOutput += supResponse.usage.outputTokens;
|
|
502
|
+
config.tokenLedger?.record({
|
|
503
|
+
ts: new Date().toISOString(),
|
|
504
|
+
sessionId: config.sessionId,
|
|
505
|
+
roleId,
|
|
506
|
+
model: config.model ?? 'unknown',
|
|
507
|
+
inputTokens: supResponse.usage.inputTokens,
|
|
508
|
+
outputTokens: supResponse.usage.outputTokens,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
messages.push({ role: 'assistant', content: supResponse.content });
|
|
512
|
+
for (const block of supResponse.content) {
|
|
513
|
+
if (block.type === 'text' && block.text) {
|
|
514
|
+
outputParts.push(block.text);
|
|
515
|
+
onText?.(block.text);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// If no tool calls, supervision is done
|
|
520
|
+
if (supResponse.stopReason !== 'tool_use') break;
|
|
521
|
+
|
|
522
|
+
// Execute tool calls
|
|
523
|
+
const supToolCalls = supResponse.content.filter(
|
|
524
|
+
(b): b is MessageContent & { type: 'tool_use' } => b.type === 'tool_use',
|
|
525
|
+
);
|
|
526
|
+
const supResults: ToolResult[] = [];
|
|
527
|
+
for (const tc of supToolCalls) {
|
|
528
|
+
allToolCalls.push({ name: tc.name, input: tc.input });
|
|
529
|
+
const result = await executeTool(
|
|
530
|
+
{ id: tc.id, name: tc.name, input: tc.input },
|
|
531
|
+
toolExecOptions,
|
|
532
|
+
);
|
|
533
|
+
supResults.push(result);
|
|
534
|
+
|
|
535
|
+
// Track additional dispatches from supervision
|
|
536
|
+
if (tc.name === 'dispatch' && !result.is_error) {
|
|
537
|
+
dispatches.push({
|
|
538
|
+
roleId: String(tc.input.roleId),
|
|
539
|
+
task: String(tc.input.task),
|
|
540
|
+
result: result.content,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
messages.push({
|
|
546
|
+
role: 'user',
|
|
547
|
+
content: supResults.map((r) => ({
|
|
548
|
+
type: 'tool_result' as const,
|
|
549
|
+
tool_use_id: r.tool_use_id,
|
|
550
|
+
content: r.content,
|
|
551
|
+
is_error: r.is_error,
|
|
552
|
+
})) as unknown as MessageContent[],
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
onTurnComplete?.(turns);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Detect file changes once — used by Phase B and Post-K
|
|
560
|
+
const hasFileChanges = allToolCalls.some((tc) =>
|
|
561
|
+
['write', 'edit', 'bash'].includes(tc.name.toLowerCase()),
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
// Phase B: Member Self-Verification — type checking + visual verification
|
|
565
|
+
// Any non-C-Level role that made file changes gets verification (no hardcoded role IDs)
|
|
566
|
+
if (!isCLevel && hasFileChanges) {
|
|
567
|
+
const verifyPrompt = [
|
|
568
|
+
'[AUTO-VERIFICATION] 작업이 완료되었습니다. 아래 검증을 수행하세요:',
|
|
569
|
+
'1. `cd src/api && npx tsc --noEmit` — 타입 에러 확인',
|
|
570
|
+
'2. `cd src/web && npx tsc --noEmit` — 프론트엔드 타입 에러 확인',
|
|
571
|
+
'3. UI/CSS 변경이 있었다면 Playwright MCP로 스크린샷을 촬영하여 시각 검증',
|
|
572
|
+
'검증 결과를 간단히 보고하세요.',
|
|
573
|
+
].join('\n');
|
|
574
|
+
|
|
575
|
+
messages.push({ role: 'user', content: verifyPrompt });
|
|
576
|
+
|
|
577
|
+
if (turns < maxTurns) {
|
|
578
|
+
turns++;
|
|
579
|
+
const verifyResponse = await llm.chat(context.systemPrompt, messages, tools, abortSignal);
|
|
580
|
+
totalInput += verifyResponse.usage.inputTokens;
|
|
581
|
+
totalOutput += verifyResponse.usage.outputTokens;
|
|
582
|
+
config.tokenLedger?.record({
|
|
583
|
+
ts: new Date().toISOString(),
|
|
584
|
+
sessionId: config.sessionId,
|
|
585
|
+
roleId,
|
|
586
|
+
model: config.model ?? 'unknown',
|
|
587
|
+
inputTokens: verifyResponse.usage.inputTokens,
|
|
588
|
+
outputTokens: verifyResponse.usage.outputTokens,
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
messages.push({ role: 'assistant', content: verifyResponse.content });
|
|
592
|
+
for (const block of verifyResponse.content) {
|
|
593
|
+
if (block.type === 'text' && block.text) {
|
|
594
|
+
outputParts.push(block.text);
|
|
595
|
+
onText?.(block.text);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Execute verification tool calls if needed
|
|
600
|
+
if (verifyResponse.stopReason === 'tool_use') {
|
|
601
|
+
const verifyToolCalls = verifyResponse.content.filter(
|
|
602
|
+
(b): b is MessageContent & { type: 'tool_use' } => b.type === 'tool_use',
|
|
603
|
+
);
|
|
604
|
+
const verifyResults: ToolResult[] = [];
|
|
605
|
+
for (const tc of verifyToolCalls) {
|
|
606
|
+
allToolCalls.push({ name: tc.name, input: tc.input });
|
|
607
|
+
const result = await executeTool(
|
|
608
|
+
{ id: tc.id, name: tc.name, input: tc.input },
|
|
609
|
+
toolExecOptions,
|
|
610
|
+
);
|
|
611
|
+
verifyResults.push(result);
|
|
612
|
+
}
|
|
613
|
+
messages.push({
|
|
614
|
+
role: 'user',
|
|
615
|
+
content: verifyResults.map((r) => ({
|
|
616
|
+
type: 'tool_result' as const,
|
|
617
|
+
tool_use_id: r.tool_use_id,
|
|
618
|
+
content: r.content,
|
|
619
|
+
is_error: r.is_error,
|
|
620
|
+
})) as unknown as MessageContent[],
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
if (turns < maxTurns) {
|
|
624
|
+
turns++;
|
|
625
|
+
const summaryResponse = await llm.chat(context.systemPrompt, messages, tools, abortSignal);
|
|
626
|
+
totalInput += summaryResponse.usage.inputTokens;
|
|
627
|
+
totalOutput += summaryResponse.usage.outputTokens;
|
|
628
|
+
config.tokenLedger?.record({
|
|
629
|
+
ts: new Date().toISOString(),
|
|
630
|
+
sessionId: config.sessionId,
|
|
631
|
+
roleId,
|
|
632
|
+
model: config.model ?? 'unknown',
|
|
633
|
+
inputTokens: summaryResponse.usage.inputTokens,
|
|
634
|
+
outputTokens: summaryResponse.usage.outputTokens,
|
|
635
|
+
});
|
|
636
|
+
for (const block of summaryResponse.content) {
|
|
637
|
+
if (block.type === 'text' && block.text) {
|
|
638
|
+
outputParts.push(block.text);
|
|
639
|
+
onText?.(block.text);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
onTurnComplete?.(turns);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ── Post-K: ④⑤ Knowledge/Task update (KP-008) ──
|
|
650
|
+
// ALL roles (C-Level and members) update journal/tasks after significant work.
|
|
651
|
+
// Runs for: members who made file changes, C-Level who dispatched work.
|
|
652
|
+
if (hasFileChanges || dispatches.length > 0) {
|
|
653
|
+
const postKPrompt = [
|
|
654
|
+
'[POST-KNOWLEDGING] 작업이 완료되었습니다. The Loop 마무리를 수행하세요:',
|
|
655
|
+
'',
|
|
656
|
+
'## ④ Knowledge 업데이트 (The Loop Step 4)',
|
|
657
|
+
'다음 중 해당하는 항목을 수행하세요:',
|
|
658
|
+
'- 본인 journal 업데이트 (`knowledge/roles/' + roleId + '/journal/YYYY-MM-DD.md` — 오늘 날짜 파일)',
|
|
659
|
+
'- 구현 중 새로 발견한 패턴/아키텍처 결정이 있다면 관련 문서 업데이트',
|
|
660
|
+
' (예: knowledge/architecture/web-app-ia.md, knowledge/architecture/session-worktree-isolation.md 등)',
|
|
661
|
+
'- 중요한 기술 결정은 knowledge/decisions/ 또는 knowledge/architecture/ 반영',
|
|
662
|
+
'',
|
|
663
|
+
'## ⑤ Task 상태 갱신 (The Loop Step 5)',
|
|
664
|
+
'- `projects/tycono-platform/tasks.md` (또는 관련 tasks 파일)에서 완료한 태스크 상태를 DONE으로 변경',
|
|
665
|
+
'- 다음 작업이 있다면 식별하여 메모',
|
|
666
|
+
'',
|
|
667
|
+
'⛔ **필수**: ④와 ⑤를 모두 수행해야 The Loop이 완료됩니다.',
|
|
668
|
+
'이제 ④⑤를 수행하세요 (Read, Edit 도구 사용).',
|
|
669
|
+
].join('\n');
|
|
670
|
+
|
|
671
|
+
messages.push({ role: 'user', content: postKPrompt });
|
|
672
|
+
|
|
673
|
+
// Run Post-K loop (최대 2턴)
|
|
674
|
+
const maxPostKRounds = 2;
|
|
675
|
+
for (let round = 0; round < maxPostKRounds && turns < maxTurns; round++) {
|
|
676
|
+
if (abortSignal?.aborted) break;
|
|
677
|
+
turns++;
|
|
678
|
+
|
|
679
|
+
const postKResponse = await llm.chat(context.systemPrompt, messages, tools, abortSignal);
|
|
680
|
+
totalInput += postKResponse.usage.inputTokens;
|
|
681
|
+
totalOutput += postKResponse.usage.outputTokens;
|
|
682
|
+
config.tokenLedger?.record({
|
|
683
|
+
ts: new Date().toISOString(),
|
|
684
|
+
sessionId: config.sessionId,
|
|
685
|
+
roleId,
|
|
686
|
+
model: config.model ?? 'unknown',
|
|
687
|
+
inputTokens: postKResponse.usage.inputTokens,
|
|
688
|
+
outputTokens: postKResponse.usage.outputTokens,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
messages.push({ role: 'assistant', content: postKResponse.content });
|
|
692
|
+
for (const block of postKResponse.content) {
|
|
693
|
+
if (block.type === 'text' && block.text) {
|
|
694
|
+
outputParts.push(block.text);
|
|
695
|
+
onText?.(block.text);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// If no tool calls, Post-K is done
|
|
700
|
+
if (postKResponse.stopReason !== 'tool_use') break;
|
|
701
|
+
|
|
702
|
+
// Execute Post-K tool calls
|
|
703
|
+
const postKToolCalls = postKResponse.content.filter(
|
|
704
|
+
(b): b is MessageContent & { type: 'tool_use' } => b.type === 'tool_use',
|
|
705
|
+
);
|
|
706
|
+
const postKResults: ToolResult[] = [];
|
|
707
|
+
for (const tc of postKToolCalls) {
|
|
708
|
+
allToolCalls.push({ name: tc.name, input: tc.input });
|
|
709
|
+
const result = await executeTool(
|
|
710
|
+
{ id: tc.id, name: tc.name, input: tc.input },
|
|
711
|
+
toolExecOptions,
|
|
712
|
+
);
|
|
713
|
+
postKResults.push(result);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
messages.push({
|
|
717
|
+
role: 'user',
|
|
718
|
+
content: postKResults.map((r) => ({
|
|
719
|
+
type: 'tool_result' as const,
|
|
720
|
+
tool_use_id: r.tool_use_id,
|
|
721
|
+
content: r.content,
|
|
722
|
+
is_error: r.is_error,
|
|
723
|
+
})) as unknown as MessageContent[],
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
onTurnComplete?.(turns);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return {
|
|
732
|
+
output: outputParts.join('\n'),
|
|
733
|
+
turns,
|
|
734
|
+
totalTokens: { input: totalInput, output: totalOutput },
|
|
735
|
+
toolCalls: allToolCalls,
|
|
736
|
+
dispatches,
|
|
737
|
+
};
|
|
738
|
+
}
|