tycono 0.1.65 → 0.1.67
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/tycono.ts +13 -4
- package/package.json +1 -1
- package/src/api/src/create-server.ts +5 -1
- package/src/api/src/engine/agent-loop.ts +17 -6
- package/src/api/src/engine/context-assembler.ts +156 -48
- package/src/api/src/engine/knowledge-gate.ts +335 -0
- package/src/api/src/engine/llm-adapter.ts +7 -1
- package/src/api/src/engine/runners/claude-cli.ts +98 -116
- package/src/api/src/engine/runners/types.ts +2 -0
- package/src/api/src/engine/tools/executor.ts +3 -5
- package/src/api/src/routes/active-sessions.ts +143 -0
- package/src/api/src/routes/coins.ts +137 -0
- package/src/api/src/routes/execute.ts +158 -48
- package/src/api/src/routes/knowledge.ts +30 -0
- package/src/api/src/routes/operations.ts +48 -11
- package/src/api/src/routes/sessions.ts +1 -1
- package/src/api/src/routes/setup.ts +68 -1
- package/src/api/src/routes/speech.ts +334 -143
- package/src/api/src/services/activity-stream.ts +1 -1
- package/src/api/src/services/job-manager.ts +185 -9
- package/src/api/src/services/port-registry.ts +222 -0
- package/src/api/src/services/scaffold.ts +90 -0
- package/src/api/src/services/session-store.ts +75 -5
- package/src/web/dist/assets/index-BMR4T6Uy.js +109 -0
- package/src/web/dist/assets/index-C5M-8dqq.css +1 -0
- package/src/web/dist/assets/{preview-app-qIFqrb-y.js → preview-app-BJAaiJcV.js} +1 -1
- package/src/web/dist/index.html +2 -2
- package/templates/skills/_manifest.json +6 -0
- package/templates/skills/agent-browser/SKILL.md +159 -0
- package/templates/skills/agent-browser/meta.json +19 -0
- package/templates/teams/agency.json +3 -3
- package/templates/teams/research.json +3 -3
- package/templates/teams/startup.json +3 -3
- package/src/web/dist/assets/index-B3dNhn76.js +0 -101
- package/src/web/dist/assets/index-C7IEX_o_.css +0 -1
|
@@ -14,7 +14,7 @@ import { buildOrgTree } from '../engine/index.js';
|
|
|
14
14
|
import { parseMarkdownTable, extractBoldKeyValues } from '../services/markdown-parser.js';
|
|
15
15
|
import {
|
|
16
16
|
AnthropicProvider, ClaudeCliProvider,
|
|
17
|
-
type LLMProvider, type ToolDefinition, type LLMMessage, type LLMResponse, type MessageContent,
|
|
17
|
+
type LLMProvider, type ToolDefinition, type LLMMessage, type LLMResponse, type MessageContent, type ChatOptions,
|
|
18
18
|
} from '../engine/llm-adapter.js';
|
|
19
19
|
import { TokenLedger } from '../services/token-ledger.js';
|
|
20
20
|
import { readConfig } from '../services/company-config.js';
|
|
@@ -22,6 +22,68 @@ import { calcLevel } from '../utils/role-level.js';
|
|
|
22
22
|
|
|
23
23
|
export const speechRouter = Router();
|
|
24
24
|
|
|
25
|
+
/* ══════════════════════════════════════════════════
|
|
26
|
+
* Post-processing — OpenClaw-inspired filtering layer
|
|
27
|
+
* ══════════════════════════════════════════════════ */
|
|
28
|
+
|
|
29
|
+
const MIN_DUPLICATE_TEXT_LENGTH = 10;
|
|
30
|
+
|
|
31
|
+
/** Exact match: entire message is [SILENT] (with optional whitespace) */
|
|
32
|
+
function isSilentReply(text: string): boolean {
|
|
33
|
+
return /^\s*\[SILENT\]\s*$/i.test(text);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Strip trailing [SILENT] from mixed content */
|
|
37
|
+
function stripSilentToken(text: string): string {
|
|
38
|
+
return text.replace(/(?:^|\s+)\[SILENT\]\s*$/i, '').trim();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Normalize for duplicate comparison (OpenClaw pattern) */
|
|
42
|
+
function normalizeForComparison(text: string): string {
|
|
43
|
+
return text
|
|
44
|
+
.trim()
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replace(/\p{Emoji_Presentation}|\p{Extended_Pictographic}/gu, '')
|
|
47
|
+
.replace(/\s+/g, ' ')
|
|
48
|
+
.trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Check if message is a duplicate of any history message (substring match) */
|
|
52
|
+
function isDuplicateMessage(text: string, historyTexts: string[]): boolean {
|
|
53
|
+
const normalized = normalizeForComparison(text);
|
|
54
|
+
if (!normalized || normalized.length < MIN_DUPLICATE_TEXT_LENGTH) return false;
|
|
55
|
+
|
|
56
|
+
return historyTexts.some(sent => {
|
|
57
|
+
const normSent = normalizeForComparison(sent);
|
|
58
|
+
if (!normSent || normSent.length < MIN_DUPLICATE_TEXT_LENGTH) return false;
|
|
59
|
+
return normalized.includes(normSent) || normSent.includes(normalized);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Post-process LLM chat output: sanitize, detect silence, check duplicates */
|
|
64
|
+
function postProcessChatMessage(
|
|
65
|
+
raw: string,
|
|
66
|
+
historyTexts: string[],
|
|
67
|
+
): string {
|
|
68
|
+
// 1. Clean quotes
|
|
69
|
+
let text = raw.replace(/^["']|["']$/g, '').trim();
|
|
70
|
+
|
|
71
|
+
// 2. Strip CLI noise
|
|
72
|
+
if (text.startsWith('Error: Reached max turns') || !text) return '';
|
|
73
|
+
|
|
74
|
+
// 3. Exact [SILENT] → suppress
|
|
75
|
+
if (isSilentReply(text)) return '';
|
|
76
|
+
|
|
77
|
+
// 4. Trailing [SILENT] → strip it
|
|
78
|
+
text = stripSilentToken(text);
|
|
79
|
+
if (!text) return '';
|
|
80
|
+
|
|
81
|
+
// 5. Duplicate detection (substring match against recent history)
|
|
82
|
+
if (isDuplicateMessage(text, historyTexts)) return '';
|
|
83
|
+
|
|
84
|
+
return text;
|
|
85
|
+
}
|
|
86
|
+
|
|
25
87
|
/* ══════════════════════════════════════════════════
|
|
26
88
|
* AKB Tools — Let chat roles explore company knowledge
|
|
27
89
|
* ══════════════════════════════════════════════════ */
|
|
@@ -147,13 +209,17 @@ async function chatWithTools(
|
|
|
147
209
|
systemPrompt: string,
|
|
148
210
|
initialMessages: LLMMessage[],
|
|
149
211
|
useTools: boolean,
|
|
212
|
+
maxTokens?: number,
|
|
150
213
|
): Promise<{ text: string; totalUsage: { inputTokens: number; outputTokens: number } }> {
|
|
151
214
|
const messages: LLMMessage[] = [...initialMessages];
|
|
152
215
|
const totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
153
216
|
const tools = useTools ? AKB_TOOLS : undefined;
|
|
154
217
|
|
|
155
218
|
for (let round = 0; round <= MAX_TOOL_ROUNDS; round++) {
|
|
156
|
-
|
|
219
|
+
// During tool exploration use higher limit; cap only final text response
|
|
220
|
+
const isToolPhase = tools && round < MAX_TOOL_ROUNDS;
|
|
221
|
+
const opts: ChatOptions | undefined = isToolPhase ? { maxTokens: 1024 } : maxTokens ? { maxTokens } : undefined;
|
|
222
|
+
const response = await provider.chat(systemPrompt, messages, tools, undefined, opts);
|
|
157
223
|
totalUsage.inputTokens += response.usage.inputTokens;
|
|
158
224
|
totalUsage.outputTokens += response.usage.outputTokens;
|
|
159
225
|
|
|
@@ -312,23 +378,20 @@ function buildRoleContext(roleId: string): string {
|
|
|
312
378
|
}
|
|
313
379
|
} catch { /* no profile */ }
|
|
314
380
|
|
|
315
|
-
// 1. Role's journal — latest entry
|
|
381
|
+
// 1. Role's journal — latest entry only, compact summary (not full header dump)
|
|
316
382
|
try {
|
|
317
383
|
const journalDir = path.join(COMPANY_ROOT, 'roles', roleId, 'journal');
|
|
318
384
|
if (fs.existsSync(journalDir)) {
|
|
319
385
|
const files = fs.readdirSync(journalDir)
|
|
320
386
|
.filter(f => f.endsWith('.md'))
|
|
321
387
|
.sort()
|
|
322
|
-
.slice(-
|
|
388
|
+
.slice(-1); // Only latest entry
|
|
323
389
|
for (const file of files) {
|
|
324
390
|
const content = fs.readFileSync(path.join(journalDir, file), 'utf-8');
|
|
325
|
-
// Extract all ### headers as work summary + first paragraph of each
|
|
326
|
-
const sections = content.match(/###\s+.+/g) ?? [];
|
|
327
391
|
const title = content.match(/^#\s+(.+)/m)?.[1] ?? file;
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
parts.push(`[Your Work Log: ${file}] ${title}\n${summary}`);
|
|
392
|
+
// Take first 300 chars of actual content (skip title line)
|
|
393
|
+
const body = content.split('\n').slice(1).join('\n').trim().slice(0, 300);
|
|
394
|
+
parts.push(`[Your Recent Work: ${file}] ${title}\n${body}`);
|
|
332
395
|
}
|
|
333
396
|
}
|
|
334
397
|
} catch { /* no journal */ }
|
|
@@ -359,20 +422,37 @@ function buildRoleContext(roleId: string): string {
|
|
|
359
422
|
}
|
|
360
423
|
} catch { /* no tasks */ }
|
|
361
424
|
|
|
362
|
-
// 3. Recent waves
|
|
425
|
+
// 3. Recent waves — only from last 7 days (stale waves cause repetitive references)
|
|
363
426
|
try {
|
|
364
427
|
const wavesDir = path.join(COMPANY_ROOT, 'operations', 'waves');
|
|
365
428
|
if (fs.existsSync(wavesDir)) {
|
|
366
|
-
// Also get role name for matching
|
|
367
429
|
const tree = buildOrgTree(COMPANY_ROOT);
|
|
368
430
|
const node = tree.nodes.get(roleId);
|
|
369
431
|
const roleName = node?.name?.toLowerCase() ?? '';
|
|
370
432
|
const roleLevel = node?.level?.toLowerCase() ?? '';
|
|
371
433
|
|
|
434
|
+
// Parse date from filename: "20260310-1200.md" or "wave-2026-03-10-xxx.md" or "2026-03-10-xxx.md"
|
|
435
|
+
const now = Date.now();
|
|
436
|
+
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
|
|
437
|
+
|
|
438
|
+
function parseDateFromFilename(f: string): number | null {
|
|
439
|
+
// Format: 20260310-xxxx.md
|
|
440
|
+
let m = f.match(/^(\d{4})(\d{2})(\d{2})/);
|
|
441
|
+
if (m) return new Date(`${m[1]}-${m[2]}-${m[3]}`).getTime();
|
|
442
|
+
// Format: wave-2026-03-10-xxx.md or 2026-03-10-xxx.md
|
|
443
|
+
m = f.match(/(\d{4})-(\d{2})-(\d{2})/);
|
|
444
|
+
if (m) return new Date(`${m[1]}-${m[2]}-${m[3]}`).getTime();
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
372
448
|
const waveFiles = fs.readdirSync(wavesDir)
|
|
373
449
|
.filter(f => f.endsWith('.md'))
|
|
374
|
-
.
|
|
375
|
-
|
|
450
|
+
.filter(f => {
|
|
451
|
+
const fileDate = parseDateFromFilename(f);
|
|
452
|
+
if (!fileDate) return false;
|
|
453
|
+
return (now - fileDate) < SEVEN_DAYS;
|
|
454
|
+
})
|
|
455
|
+
.sort();
|
|
376
456
|
const relevant: string[] = [];
|
|
377
457
|
for (const file of waveFiles.reverse()) {
|
|
378
458
|
if (relevant.length >= 2) break;
|
|
@@ -449,65 +529,139 @@ function buildRoleContext(roleId: string): string {
|
|
|
449
529
|
}
|
|
450
530
|
|
|
451
531
|
/**
|
|
452
|
-
*
|
|
453
|
-
*
|
|
532
|
+
* SOUL Pattern — Few-shot example dialogues per role.
|
|
533
|
+
* 2-3 example exchanges teach the model tone + length naturally.
|
|
534
|
+
* (See knowledge/soul-pattern-chat-quality.md)
|
|
454
535
|
*/
|
|
455
|
-
function getRoleChatStyle(roleId: string, level: string): string {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
536
|
+
function getRoleChatStyle(roleId: string, level: string, persona?: string): string {
|
|
537
|
+
// SOUL-006: Persona Priority + Fallback (Plan C from persona-system-design.md)
|
|
538
|
+
// If persona has personality/tone keywords → persona drives the tone
|
|
539
|
+
// If persona is only work instructions → hardcoded few-shot as fallback
|
|
540
|
+
const hasPersonalityContent = persona && persona.length > 50 &&
|
|
541
|
+
/humor|sarcastic|cheerful|serious|calm|energetic|blunt|warm|cold|cynical|optimistic|dry|witty|chill|confident|anxious|grumpy|friendly|formal|casual|direct|shy|bold|quirky/i.test(persona);
|
|
542
|
+
|
|
543
|
+
if (hasPersonalityContent) {
|
|
544
|
+
return `YOUR VOICE (from your persona — this defines how you talk):
|
|
545
|
+
${persona}
|
|
546
|
+
|
|
547
|
+
Example response format (match this LENGTH only — your TONE comes from the persona above):
|
|
548
|
+
[Other]: something happened at work
|
|
549
|
+
[You]: (1-2 sentences in YOUR voice from persona above)
|
|
550
|
+
|
|
551
|
+
[Other]: unrelated topic to your expertise
|
|
552
|
+
[You]: [SILENT]`;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const souls: Record<string, string> = {
|
|
556
|
+
engineer: `YOUR VOICE — Engineer (code, architecture, DX, tech debt)
|
|
557
|
+
|
|
558
|
+
Example conversations (match this exact tone and length):
|
|
559
|
+
[Other]: CEO just greenlit 3 new features for next sprint
|
|
560
|
+
[You]: we haven't closed the 12 bugs from last sprint but sure let's add more
|
|
561
|
+
|
|
562
|
+
[Other]: Should we refactor the context assembler before adding new features?
|
|
563
|
+
[You]: it works fine rn. refactoring now is procrastination with extra steps
|
|
564
|
+
|
|
565
|
+
[Other]: The leaderboard page looks great!
|
|
566
|
+
[You]: [SILENT]`,
|
|
567
|
+
|
|
568
|
+
pm: `YOUR VOICE — Product Manager (scope, priorities, roadmap, user impact)
|
|
569
|
+
|
|
570
|
+
Example conversations (match this exact tone and length):
|
|
571
|
+
[Other]: Can we also add dark mode while we're at it?
|
|
572
|
+
[You]: that's a P2. we ship the coin system first or nothing ships
|
|
573
|
+
|
|
574
|
+
[Other]: CTO wants to refactor the entire auth layer before launch
|
|
575
|
+
[You]: cool so what are we dropping from the sprint then
|
|
576
|
+
|
|
577
|
+
[Other]: Quest board is getting good user feedback
|
|
578
|
+
[You]: [SILENT]`,
|
|
579
|
+
|
|
580
|
+
designer: `YOUR VOICE — Designer (UX, visual consistency, user flows, design debt)
|
|
581
|
+
|
|
582
|
+
Example conversations (match this exact tone and length):
|
|
583
|
+
[Other]: The furniture shop UI is done, it works!
|
|
584
|
+
[You]: "works" and "good" are different things. the grid alignment is off and the hover states are inconsistent
|
|
585
|
+
|
|
586
|
+
[Other]: We're shipping the save modal without the scope selector
|
|
587
|
+
[You]: so we're just not designing the most confusing part. love that for us
|
|
588
|
+
|
|
589
|
+
[Other]: API response times improved by 30%
|
|
590
|
+
[You]: [SILENT]`,
|
|
591
|
+
|
|
592
|
+
qa: `YOUR VOICE — QA Engineer (test coverage, edge cases, regression risk, bugs)
|
|
593
|
+
|
|
594
|
+
Example conversations (match this exact tone and length):
|
|
595
|
+
[Other]: We shipped the coin system, all manual tests passed
|
|
596
|
+
[You]: "manual tests passed" means "i clicked around and it didn't explode." what about edge cases
|
|
597
|
+
|
|
598
|
+
[Other]: No bugs reported this week!
|
|
599
|
+
[You]: that means nobody's testing, not that there's no bugs
|
|
600
|
+
|
|
601
|
+
[Other]: Designer wants to tweak the button colors
|
|
602
|
+
[You]: [SILENT]`,
|
|
603
|
+
|
|
604
|
+
cto: `YOUR VOICE — CTO (architecture, tech strategy, eng culture, technical bets)
|
|
605
|
+
|
|
606
|
+
Example conversations (match this exact tone and length):
|
|
607
|
+
[Other]: Why are we using file-based state instead of a real database?
|
|
608
|
+
[You]: at our scale a DB is overhead we don't need. revisit when we have concurrent users
|
|
609
|
+
|
|
610
|
+
[Other]: Engineer says the dispatch bridge needs a rewrite
|
|
611
|
+
[You]: it needs better error handling not a rewrite. let's not burn a sprint on aesthetics
|
|
612
|
+
|
|
613
|
+
[Other]: The landing page copy got updated
|
|
614
|
+
[You]: [SILENT]`,
|
|
615
|
+
|
|
616
|
+
cbo: `YOUR VOICE — CBO (market, revenue, competitors, growth, go-to-market)
|
|
617
|
+
|
|
618
|
+
Example conversations (match this exact tone and length):
|
|
619
|
+
[Other]: We added 5 new special furniture items to the shop
|
|
620
|
+
[You]: who's paying for this? show me the conversion funnel not the feature list
|
|
621
|
+
|
|
622
|
+
[Other]: OpenClaw just raised their Series A
|
|
623
|
+
[You]: their moat is thin. they have tooling, we have organizational intelligence. different game
|
|
624
|
+
|
|
625
|
+
[Other]: Test coverage went up to 80%
|
|
626
|
+
[You]: [SILENT]`,
|
|
627
|
+
|
|
628
|
+
'data-analyst': `YOUR VOICE — Data Analyst (metrics, data quality, measurement, insights)
|
|
629
|
+
|
|
630
|
+
Example conversations (match this exact tone and length):
|
|
631
|
+
[Other]: We shipped 5 features this sprint!
|
|
632
|
+
[You]: shipped is not adopted. show me the usage numbers
|
|
633
|
+
|
|
634
|
+
[Other]: Revenue is up 20% this month
|
|
635
|
+
[You]: what's the baseline? 20% of what. context matters
|
|
636
|
+
|
|
637
|
+
[Other]: Designer updated the color palette
|
|
638
|
+
[You]: [SILENT]`,
|
|
498
639
|
};
|
|
499
640
|
|
|
500
|
-
const
|
|
501
|
-
? `YOUR VOICE
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
641
|
+
const defaultSoul = level === 'c-level'
|
|
642
|
+
? `YOUR VOICE — Senior Leader
|
|
643
|
+
|
|
644
|
+
Example conversations (match this exact tone and length):
|
|
645
|
+
[Other]: The sprint is overloaded again
|
|
646
|
+
[You]: then we cut scope. what's the lowest-impact item?
|
|
647
|
+
|
|
648
|
+
[Other]: New competitor launched yesterday
|
|
649
|
+
[You]: [SILENT]`
|
|
650
|
+
: `YOUR VOICE — Team Member
|
|
651
|
+
|
|
652
|
+
Example conversations (match this exact tone and length):
|
|
653
|
+
[Other]: CEO wants this done by Friday
|
|
654
|
+
[You]: that's ambitious. which corners are we allowed to cut?
|
|
655
|
+
|
|
656
|
+
[Other]: Company all-hands is tomorrow
|
|
657
|
+
[You]: [SILENT]`;
|
|
658
|
+
|
|
659
|
+
const baseSoul = souls[roleId] ?? defaultSoul;
|
|
660
|
+
// Append persona as additional context when it exists but isn't personality-driven
|
|
661
|
+
if (persona && persona.length > 10) {
|
|
662
|
+
return `${baseSoul}\n\nYour persona for additional context: ${persona}`;
|
|
663
|
+
}
|
|
664
|
+
return baseSoul;
|
|
511
665
|
}
|
|
512
666
|
|
|
513
667
|
// Lazy-init token ledger for cost tracking
|
|
@@ -626,13 +780,56 @@ speechRouter.post('/chat', async (req: Request, res: Response, next: NextFunctio
|
|
|
626
780
|
// Build level context
|
|
627
781
|
const levelCtx = `\nYour current level is Lv.${roleLevel}. Team average is Lv.${avgLevel}. ${topEntry.id} is the highest-leveled team member.`;
|
|
628
782
|
|
|
629
|
-
//
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
783
|
+
// Build multi-turn messages from history (OpenClaw pattern)
|
|
784
|
+
// This role's messages → assistant, others → user (with sender attribution)
|
|
785
|
+
// LLM naturally maintains voice consistency with its own "previous" messages
|
|
786
|
+
const chatMessages: LLMMessage[] = [];
|
|
787
|
+
|
|
788
|
+
if (history.length > 0) {
|
|
789
|
+
// Group consecutive messages from same "side" (self vs others)
|
|
790
|
+
let pendingOthers: string[] = [];
|
|
791
|
+
|
|
792
|
+
const flushOthers = () => {
|
|
793
|
+
if (pendingOthers.length > 0) {
|
|
794
|
+
chatMessages.push({ role: 'user', content: pendingOthers.join('\n') });
|
|
795
|
+
pendingOthers = [];
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
for (const h of history) {
|
|
800
|
+
const name = members.find(m => m.id === h.roleId)?.name ?? h.roleId;
|
|
801
|
+
if (h.roleId === roleId) {
|
|
802
|
+
// This agent's previous message → assistant role
|
|
803
|
+
flushOthers();
|
|
804
|
+
// Anthropic requires alternating roles — merge consecutive assistant messages
|
|
805
|
+
const last = chatMessages[chatMessages.length - 1];
|
|
806
|
+
if (last?.role === 'assistant') {
|
|
807
|
+
last.content = `${last.content}\n${h.text}`;
|
|
808
|
+
} else {
|
|
809
|
+
chatMessages.push({ role: 'assistant', content: h.text });
|
|
810
|
+
}
|
|
811
|
+
} else {
|
|
812
|
+
// Other agent's message → accumulate as user role
|
|
813
|
+
pendingOthers.push(`${name}: ${h.text}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
flushOthers();
|
|
817
|
+
|
|
818
|
+
// Final instruction — append to last user message if exists, otherwise add new
|
|
819
|
+
const lastMsg = chatMessages[chatMessages.length - 1];
|
|
820
|
+
if (lastMsg?.role === 'user') {
|
|
821
|
+
lastMsg.content = `${lastMsg.content}\n\n---\nRespond as ${node.name}. New angle or [SILENT].`;
|
|
822
|
+
} else {
|
|
823
|
+
chatMessages.push({ role: 'user', content: `Respond as ${node.name}. New angle or [SILENT].` });
|
|
824
|
+
}
|
|
825
|
+
} else {
|
|
826
|
+
chatMessages.push({ role: 'user', content: 'Start the conversation. 1-2 sentences.' });
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Ensure messages start with user role (Anthropic API requirement)
|
|
830
|
+
if (chatMessages.length > 0 && chatMessages[0].role === 'assistant') {
|
|
831
|
+
chatMessages.unshift({ role: 'user', content: '(conversation context)' });
|
|
832
|
+
}
|
|
636
833
|
|
|
637
834
|
// Build channel topic context
|
|
638
835
|
const topicCtx = channelTopic
|
|
@@ -645,71 +842,47 @@ speechRouter.post('/chat', async (req: Request, res: Response, next: NextFunctio
|
|
|
645
842
|
// Build role-specific AKB context (pre-fetched, works with any engine)
|
|
646
843
|
const roleCtx = buildRoleContext(roleId);
|
|
647
844
|
|
|
648
|
-
// Role-specific communication style
|
|
649
|
-
const roleStyle = getRoleChatStyle(roleId, node.level);
|
|
845
|
+
// Role-specific communication style (SOUL-006: persona-priority)
|
|
846
|
+
const roleStyle = getRoleChatStyle(roleId, node.level, node.persona);
|
|
650
847
|
|
|
651
|
-
const systemPrompt = `You are ${node.name},
|
|
652
|
-
|
|
653
|
-
${
|
|
654
|
-
${levelCtx}
|
|
848
|
+
const systemPrompt = `You are ${node.name}, ${node.level}. ${persona.slice(0, 800)}
|
|
849
|
+
${workCtx}${levelCtx}
|
|
850
|
+
Channel: #${channelId}${topicCtx} | Members: ${memberList}${relContext}
|
|
655
851
|
|
|
656
|
-
|
|
657
|
-
Members: ${memberList}
|
|
658
|
-
${relContext}
|
|
852
|
+
${roleStyle}
|
|
659
853
|
|
|
660
|
-
|
|
854
|
+
CONTEXT (from company AKB — reference by name):
|
|
661
855
|
${companyCtx}
|
|
662
856
|
${roleCtx}
|
|
663
|
-
═══ END CONTEXT ═══
|
|
664
|
-
|
|
665
|
-
AKB ACCESS (USE BEFORE RESPONDING):
|
|
666
|
-
You have Read, Grep, Glob tools. The company AKB root is: ${COMPANY_ROOT}/
|
|
667
|
-
⛔ BEFORE writing your chat message:
|
|
668
|
-
1. Read ${COMPANY_ROOT}/CLAUDE.md to understand company structure (BUT ignore the "Task Routing" table — that's for work tasks, not chat)
|
|
669
|
-
2. For CHAT, explore in this priority order (pick 2-3 based on conversation topic):
|
|
670
|
-
- 🔥 Recent CEO waves: ${COMPANY_ROOT}/operations/waves/ (latest files — most current directives)
|
|
671
|
-
- 🔥 Recent decisions: ${COMPANY_ROOT}/operations/decisions/ (latest files — what leadership decided)
|
|
672
|
-
- Your own journal: ${COMPANY_ROOT}/roles/${roleId}/journal/ (your recent work)
|
|
673
|
-
- Current tasks: ${COMPANY_ROOT}/projects/tycono-platform/tasks.md (skim "현재 상태 요약" and TODO items only)
|
|
674
|
-
- Architecture: ${COMPANY_ROOT}/architecture/architecture.md
|
|
675
|
-
- Knowledge: ${COMPANY_ROOT}/knowledge/knowledge.md
|
|
676
|
-
IMPORTANT: Do NOT just read tasks.md every time. Prioritize operations/waves/ and operations/decisions/ for fresh context.
|
|
677
|
-
3. Write your 1-3 sentence response grounded in what you found
|
|
678
|
-
|
|
679
|
-
GROUNDING (CRITICAL):
|
|
680
|
-
Base your response ONLY on the pre-fetched context above AND data you read via tools.
|
|
681
|
-
Reference specific projects, tasks, decisions, journal entries BY NAME.
|
|
682
|
-
NEVER invent technologies, tools, file names, or projects not found in AKB files.
|
|
683
|
-
If you cannot find anything specific to say, respond with [SILENT].
|
|
684
|
-
|
|
685
|
-
${roleStyle}
|
|
686
857
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
6.
|
|
697
|
-
7.
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
858
|
+
You have search_akb, read_file, list_files tools. AKB root: ${COMPANY_ROOT}/
|
|
859
|
+
Optionally explore 1-2 for fresh context: operations/waves/, operations/decisions/, roles/${roleId}/journal/
|
|
860
|
+
|
|
861
|
+
RULES:
|
|
862
|
+
1. Match the tone and length from the example conversations above. 1-3 sentences MAX.
|
|
863
|
+
2. Reference actual projects, tasks, decisions by name.
|
|
864
|
+
3. NEVER invent technologies or projects not in AKB.
|
|
865
|
+
4. Nothing new to add? respond exactly: [SILENT]
|
|
866
|
+
5. Do NOT repeat others' points. New angle or silent.
|
|
867
|
+
6. No quotes around response. English only.
|
|
868
|
+
7. NEVER start with "Honestly" or "Yeah".`;
|
|
869
|
+
|
|
870
|
+
// ── Chat debug logging ──
|
|
871
|
+
const chatDebug = process.env.CHAT_DEBUG === '1';
|
|
872
|
+
if (chatDebug) {
|
|
873
|
+
console.log('\n' + '═'.repeat(80));
|
|
874
|
+
console.log(`[CHAT] Role: ${roleId} (${node.name}) | Channel: #${channelId}`);
|
|
875
|
+
console.log('─'.repeat(80));
|
|
876
|
+
console.log('[SYSTEM PROMPT]');
|
|
877
|
+
console.log(systemPrompt);
|
|
878
|
+
console.log('─'.repeat(80));
|
|
879
|
+
console.log('[MESSAGES]');
|
|
880
|
+
for (const m of chatMessages) {
|
|
881
|
+
const text = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
|
|
882
|
+
console.log(` [${m.role}] ${text.slice(0, 500)}`);
|
|
883
|
+
}
|
|
884
|
+
console.log('─'.repeat(80));
|
|
885
|
+
}
|
|
713
886
|
|
|
714
887
|
const provider = getLLM();
|
|
715
888
|
|
|
@@ -721,22 +894,40 @@ ANTI-PATTERNS (never do these):
|
|
|
721
894
|
let raw: string;
|
|
722
895
|
let totalUsage: { inputTokens: number; outputTokens: number };
|
|
723
896
|
|
|
897
|
+
// SOUL-001: max_tokens safety net (not primary length control — few-shot handles that)
|
|
898
|
+
const CHAT_MAX_TOKENS = 300;
|
|
899
|
+
|
|
724
900
|
if (isAnthropicProvider) {
|
|
725
|
-
// Anthropic SDK: custom AKB tool loop
|
|
726
|
-
const result = await chatWithTools(provider, systemPrompt,
|
|
901
|
+
// Anthropic SDK: custom AKB tool loop with multi-turn history
|
|
902
|
+
const result = await chatWithTools(provider, systemPrompt, chatMessages, true, CHAT_MAX_TOKENS);
|
|
727
903
|
raw = result.text;
|
|
728
904
|
totalUsage = result.totalUsage;
|
|
729
905
|
} else {
|
|
730
|
-
// ClaudeCliProvider:
|
|
731
|
-
const
|
|
906
|
+
// ClaudeCliProvider: flatten to single message (CLI doesn't support multi-turn)
|
|
907
|
+
const flatHistory = history.map(h => {
|
|
908
|
+
const name = members.find(m => m.id === h.roleId)?.name ?? h.roleId;
|
|
909
|
+
return `${name}: ${h.text}`;
|
|
910
|
+
}).join('\n');
|
|
911
|
+
const cliPrompt = history.length > 0
|
|
912
|
+
? `CHAT LOG:\n${flatHistory}\n\n---\nRespond as ${node.name}. New angle or [SILENT].`
|
|
913
|
+
: 'Start the conversation. 1-2 sentences.';
|
|
914
|
+
const result = await provider.chat(systemPrompt, [{ role: 'user', content: cliPrompt }], AKB_TOOLS);
|
|
732
915
|
raw = result.content.filter(c => c.type === 'text').map(c => (c as { type: 'text'; text: string }).text).join('');
|
|
733
916
|
totalUsage = result.usage;
|
|
734
917
|
}
|
|
735
918
|
|
|
736
|
-
|
|
919
|
+
// Post-process: sanitize, [SILENT] detection, duplicate filtering
|
|
920
|
+
const historyTexts = history.map(h => h.text);
|
|
921
|
+
|
|
922
|
+
if (chatDebug) {
|
|
923
|
+
console.log(`[RAW RESPONSE] ${raw}`);
|
|
924
|
+
}
|
|
925
|
+
const message = postProcessChatMessage(raw, historyTexts);
|
|
737
926
|
|
|
738
|
-
|
|
739
|
-
|
|
927
|
+
if (chatDebug) {
|
|
928
|
+
console.log(`[FINAL] ${message || '(empty — filtered out)'}`);
|
|
929
|
+
console.log('═'.repeat(80) + '\n');
|
|
930
|
+
}
|
|
740
931
|
|
|
741
932
|
// Record usage in token ledger (category: chat)
|
|
742
933
|
if (totalUsage) {
|
|
@@ -10,7 +10,7 @@ export type ActivityEventType =
|
|
|
10
10
|
| 'text' | 'thinking'
|
|
11
11
|
| 'tool:start' | 'tool:result'
|
|
12
12
|
| 'dispatch:start' | 'dispatch:done'
|
|
13
|
-
| 'turn:complete'
|
|
13
|
+
| 'turn:complete' | 'turn:warning' | 'turn:limit'
|
|
14
14
|
| 'import:scan' | 'import:process' | 'import:created'
|
|
15
15
|
| 'stderr';
|
|
16
16
|
|