tycono 0.1.28 → 0.1.30
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/LICENSE +1 -1
- package/README.md +61 -18
- package/bin/tycono.ts +11 -4
- package/package.json +8 -4
- package/src/api/src/engine/context-assembler.ts +2 -2
- package/src/api/src/engine/runners/claude-cli.ts +4 -2
- package/src/api/src/routes/knowledge.ts +4 -4
- package/src/api/src/routes/speech.ts +198 -18
- package/src/web/dist/assets/index-BUQBF_9Z.css +1 -0
- package/src/web/dist/assets/index-bBNMD9Hr.js +100 -0
- package/src/web/dist/assets/{preview-app-LDESpiII.js → preview-app-BXTCBNgF.js} +1 -1
- package/src/web/dist/index.html +2 -2
- package/templates/CLAUDE.md.tmpl +43 -10
- package/src/web/dist/assets/index-BdKLLYdc.css +0 -1
- package/src/web/dist/assets/index-CgztWO_G.js +0 -100
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src=".github/assets/hero-office.png" alt="Tycono — AI Office" width="640" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
>
|
|
5
|
+
<h1 align="center">tycono</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Build an AI company. Watch them work.</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/tycono"><img src="https://img.shields.io/npm/v/tycono.svg" alt="npm version" /></a>
|
|
13
|
+
<a href="https://github.com/seongsu-kang/tycono/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/tycono.svg" alt="license" /></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/tycono"><img src="https://img.shields.io/node/v/tycono.svg" alt="node version" /></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a href="https://tycono.ai">Website</a> ·
|
|
19
|
+
<a href="#quick-start">Quick Start</a> ·
|
|
20
|
+
<a href="#how-it-works">How It Works</a> ·
|
|
21
|
+
<a href="CONTRIBUTING.md">Contributing</a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
---
|
|
4
25
|
|
|
5
26
|
**tycono** is an open-source platform that lets you create and run an AI-powered organization. Define roles (CTO, PM, Engineer...), assign them AI agents, and watch them collaborate through a real-time dashboard.
|
|
6
27
|
|
|
@@ -13,15 +34,29 @@ npx tycono
|
|
|
13
34
|
|
|
14
35
|
That's it. A setup wizard guides you through creating your company, then your browser opens to a live dashboard showing your AI team at work.
|
|
15
36
|
|
|
37
|
+
## Why Tycono?
|
|
38
|
+
|
|
39
|
+
| | Single AI Agent | Tycono |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| **Structure** | One agent, one context | Multiple roles with org hierarchy |
|
|
42
|
+
| **Knowledge** | Loses context between sessions | Persistent, file-based knowledge system (AKB) |
|
|
43
|
+
| **Authority** | Can do anything | Scoped — each role has boundaries |
|
|
44
|
+
| **Delegation** | Manual prompt chaining | Automatic dispatch through org chart |
|
|
45
|
+
| **Visibility** | Terminal output | Real-time isometric office dashboard |
|
|
46
|
+
|
|
16
47
|
## What You Get
|
|
17
48
|
|
|
18
49
|
- **Role-based AI agents** — Each role has its own persona, authority scope, and knowledge boundaries
|
|
19
50
|
- **Org hierarchy** — Roles report to each other. CTO dispatches to Engineers. PM coordinates with Design.
|
|
20
|
-
- **Real-time dashboard** — Watch your AI team work in an isometric office
|
|
51
|
+
- **Real-time dashboard** — Watch your AI team work in an isometric pixel-art office
|
|
21
52
|
- **Knowledge management** — Automatic document routing, cross-linking, and Hub-based organization
|
|
22
53
|
- **Local-first** — Everything runs on your machine. Your data stays yours.
|
|
23
54
|
- **BYOK** — Bring your own Anthropic API key. No middleman.
|
|
24
55
|
|
|
56
|
+
<p align="center">
|
|
57
|
+
<img src=".github/assets/sidepanel-chat.png" alt="Tycono Dashboard" width="640" />
|
|
58
|
+
</p>
|
|
59
|
+
|
|
25
60
|
## Requirements
|
|
26
61
|
|
|
27
62
|
- Node.js >= 18
|
|
@@ -29,14 +64,14 @@ That's it. A setup wizard guides you through creating your company, then your br
|
|
|
29
64
|
|
|
30
65
|
## Team Templates
|
|
31
66
|
|
|
32
|
-
During setup, pick a template:
|
|
67
|
+
During setup, pick a template or build your own:
|
|
33
68
|
|
|
34
|
-
| Template | Roles |
|
|
35
|
-
|
|
36
|
-
| **Startup** | CTO + PM + Engineer |
|
|
37
|
-
| **Research** | Lead Researcher + Analyst + Writer |
|
|
38
|
-
| **Agency** | Creative Director + Designer + Developer |
|
|
39
|
-
| **Custom** | Start
|
|
69
|
+
| Template | Roles | Best For |
|
|
70
|
+
|----------|-------|----------|
|
|
71
|
+
| **Startup** | CTO + PM + Engineer | Product development |
|
|
72
|
+
| **Research** | Lead Researcher + Analyst + Writer | Analysis & reports |
|
|
73
|
+
| **Agency** | Creative Director + Designer + Developer | Client projects |
|
|
74
|
+
| **Custom** | Start empty, hire as you go | Full control |
|
|
40
75
|
|
|
41
76
|
## How It Works
|
|
42
77
|
|
|
@@ -54,25 +89,26 @@ Every role has:
|
|
|
54
89
|
- `profile.md` — Public-facing description
|
|
55
90
|
- `journal/` — Work history
|
|
56
91
|
|
|
57
|
-
##
|
|
92
|
+
## Your Company Structure
|
|
58
93
|
|
|
59
94
|
```
|
|
60
95
|
your-company/
|
|
61
|
-
├── CLAUDE.md ← AI entry point
|
|
96
|
+
├── CLAUDE.md ← AI entry point (auto-managed)
|
|
62
97
|
├── company/ ← Mission, vision, values
|
|
63
98
|
├── roles/ ← AI role definitions
|
|
64
99
|
├── projects/ ← Product specs and tasks
|
|
65
100
|
├── architecture/ ← Technical decisions
|
|
66
|
-
├── operations/ ← Standups, decisions
|
|
67
|
-
|
|
101
|
+
├── operations/ ← Standups, decisions, waves
|
|
102
|
+
├── knowledge/ ← Domain knowledge
|
|
103
|
+
└── .tycono/ ← Config and preferences
|
|
68
104
|
```
|
|
69
105
|
|
|
70
106
|
## CLI Usage
|
|
71
107
|
|
|
72
108
|
```bash
|
|
73
|
-
tycono # Start server + open dashboard
|
|
74
|
-
tycono --help
|
|
75
|
-
tycono --version
|
|
109
|
+
npx tycono # Start server + open dashboard
|
|
110
|
+
npx tycono --help # Show help
|
|
111
|
+
npx tycono --version # Show version
|
|
76
112
|
```
|
|
77
113
|
|
|
78
114
|
## Environment Variables
|
|
@@ -103,10 +139,17 @@ npm run typecheck
|
|
|
103
139
|
|
|
104
140
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
105
141
|
|
|
142
|
+
## Get Help
|
|
143
|
+
|
|
144
|
+
- [GitHub Issues](https://github.com/seongsu-kang/tycono/issues) — Bug reports and feature requests
|
|
145
|
+
- [GitHub Discussions](https://github.com/seongsu-kang/tycono/discussions) — Questions and ideas
|
|
146
|
+
|
|
106
147
|
## License
|
|
107
148
|
|
|
108
149
|
[MIT](LICENSE)
|
|
109
150
|
|
|
110
151
|
---
|
|
111
152
|
|
|
112
|
-
|
|
153
|
+
<p align="center">
|
|
154
|
+
<sub>Built with tycono. An AI company that builds itself.</sub>
|
|
155
|
+
</p>
|
package/bin/tycono.ts
CHANGED
|
@@ -131,13 +131,20 @@ async function startServer(): Promise<void> {
|
|
|
131
131
|
const port = process.env.PORT ? Number(process.env.PORT) : await findFreePort();
|
|
132
132
|
process.env.PORT = String(port);
|
|
133
133
|
|
|
134
|
-
// Detect company name from CLAUDE.md
|
|
134
|
+
// Detect company name from company/company.md (user-owned), fallback to CLAUDE.md
|
|
135
135
|
let companyName = 'My Company';
|
|
136
136
|
if (initialized) {
|
|
137
137
|
try {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
const companyMdPath = path.join(process.env.COMPANY_ROOT!, 'company', 'company.md');
|
|
139
|
+
if (fs.existsSync(companyMdPath)) {
|
|
140
|
+
const companyContent = fs.readFileSync(companyMdPath, 'utf-8');
|
|
141
|
+
const titleMatch = companyContent.match(/^#\s+(.+)/m);
|
|
142
|
+
if (titleMatch) companyName = titleMatch[1].trim();
|
|
143
|
+
} else {
|
|
144
|
+
const claudeContent = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
145
|
+
const titleMatch = claudeContent.match(/^#\s+(.+)/m);
|
|
146
|
+
if (titleMatch) companyName = titleMatch[1].trim();
|
|
147
|
+
}
|
|
141
148
|
} catch {
|
|
142
149
|
// ignore
|
|
143
150
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tycono",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"description": "Build an AI company. Watch them work.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -45,16 +45,20 @@
|
|
|
45
45
|
},
|
|
46
46
|
"keywords": [
|
|
47
47
|
"ai",
|
|
48
|
+
"tycono",
|
|
48
49
|
"company",
|
|
49
50
|
"organization",
|
|
50
51
|
"roles",
|
|
51
52
|
"claude",
|
|
52
|
-
"agent"
|
|
53
|
+
"agent",
|
|
54
|
+
"multi-agent",
|
|
55
|
+
"agentic",
|
|
56
|
+
"local-first"
|
|
53
57
|
],
|
|
54
58
|
"license": "MIT",
|
|
55
59
|
"repository": {
|
|
56
60
|
"type": "git",
|
|
57
|
-
"url": "git+https://github.com/seongsu-kang/
|
|
61
|
+
"url": "git+https://github.com/seongsu-kang/tycono.git"
|
|
58
62
|
},
|
|
59
|
-
"homepage": "https://github.com/seongsu-kang/
|
|
63
|
+
"homepage": "https://github.com/seongsu-kang/tycono#readme"
|
|
60
64
|
}
|
|
@@ -57,10 +57,10 @@ export function assembleContext(
|
|
|
57
57
|
|
|
58
58
|
const sections: string[] = [];
|
|
59
59
|
|
|
60
|
-
// 1. CLAUDE.md
|
|
60
|
+
// 1. Company Rules (CLAUDE.md + custom-rules.md + company.md)
|
|
61
61
|
const companyRules = loadCompanyRules(companyRoot);
|
|
62
62
|
if (companyRules) {
|
|
63
|
-
sections.push(
|
|
63
|
+
sections.push(companyRules);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// 2. Org Context
|
|
@@ -163,8 +163,10 @@ export class ClaudeCliRunner implements ExecutionRunner {
|
|
|
163
163
|
mcpServers: {
|
|
164
164
|
playwright: {
|
|
165
165
|
type: 'stdio',
|
|
166
|
-
command:
|
|
167
|
-
args:
|
|
166
|
+
command: process.env.PLAYWRIGHT_MCP_PATH || 'npx',
|
|
167
|
+
args: process.env.PLAYWRIGHT_MCP_PATH
|
|
168
|
+
? ['--output-dir', runnerOutputDir]
|
|
169
|
+
: ['@anthropic-ai/mcp-playwright', '--output-dir', runnerOutputDir],
|
|
168
170
|
},
|
|
169
171
|
},
|
|
170
172
|
});
|
|
@@ -206,8 +206,8 @@ knowledgeRouter.put('/{*path}', (req: Request, res: Response, next: NextFunction
|
|
|
206
206
|
return;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
const absPath = path.
|
|
210
|
-
if (!absPath.startsWith(companyRoot())) {
|
|
209
|
+
const absPath = path.resolve(companyRoot(), docId);
|
|
210
|
+
if (!absPath.startsWith(companyRoot() + path.sep) && absPath !== companyRoot()) {
|
|
211
211
|
res.status(403).json({ error: 'Forbidden' });
|
|
212
212
|
return;
|
|
213
213
|
}
|
|
@@ -248,8 +248,8 @@ knowledgeRouter.delete('/{*path}', (req: Request, res: Response, next: NextFunct
|
|
|
248
248
|
return;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
const absPath = path.
|
|
252
|
-
if (!absPath.startsWith(companyRoot())) {
|
|
251
|
+
const absPath = path.resolve(companyRoot(), docId);
|
|
252
|
+
if (!absPath.startsWith(companyRoot() + path.sep) && absPath !== companyRoot()) {
|
|
253
253
|
res.status(403).json({ error: 'Forbidden' });
|
|
254
254
|
return;
|
|
255
255
|
}
|
|
@@ -3,25 +3,187 @@
|
|
|
3
3
|
*
|
|
4
4
|
* POST /api/speech/chat — History-aware channel conversation.
|
|
5
5
|
* AI reads channel history and responds in character.
|
|
6
|
-
* Uses Haiku for
|
|
6
|
+
* Uses Haiku with AKB tool-use for grounded, context-aware chat.
|
|
7
7
|
*/
|
|
8
8
|
import { Router, Request, Response, NextFunction } from 'express';
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { glob } from 'glob';
|
|
11
12
|
import { COMPANY_ROOT, readFile, fileExists, listFiles } from '../services/file-reader.js';
|
|
12
13
|
import { buildOrgTree } from '../engine/index.js';
|
|
13
14
|
import { parseMarkdownTable, extractBoldKeyValues } from '../services/markdown-parser.js';
|
|
14
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
AnthropicProvider, ClaudeCliProvider,
|
|
17
|
+
type LLMProvider, type ToolDefinition, type LLMMessage, type LLMResponse, type MessageContent,
|
|
18
|
+
} from '../engine/llm-adapter.js';
|
|
15
19
|
import { TokenLedger } from '../services/token-ledger.js';
|
|
16
20
|
import { readConfig } from '../services/company-config.js';
|
|
17
21
|
import { calcLevel } from '../utils/role-level.js';
|
|
18
22
|
|
|
19
23
|
export const speechRouter = Router();
|
|
20
24
|
|
|
25
|
+
/* ══════════════════════════════════════════════════
|
|
26
|
+
* AKB Tools — Let chat roles explore company knowledge
|
|
27
|
+
* ══════════════════════════════════════════════════ */
|
|
28
|
+
|
|
29
|
+
const MAX_TOOL_ROUNDS = 2;
|
|
30
|
+
const MAX_FILE_CHARS = 1500; // truncate large files
|
|
31
|
+
|
|
32
|
+
const AKB_TOOLS: ToolDefinition[] = [
|
|
33
|
+
{
|
|
34
|
+
name: 'search_akb',
|
|
35
|
+
description: 'Search the company knowledge base (AKB) for keywords. Returns matching file paths and snippets. Use to find decisions, journals, projects, waves, standups, or any company knowledge.',
|
|
36
|
+
input_schema: {
|
|
37
|
+
type: 'object' as const,
|
|
38
|
+
properties: {
|
|
39
|
+
query: { type: 'string', description: 'Search keywords (e.g. "landing deploy", "refactoring decision", "Store Import")' },
|
|
40
|
+
path: { type: 'string', description: 'Optional subdirectory to search in (e.g. "operations/decisions", "projects", "knowledge"). Defaults to entire AKB.' },
|
|
41
|
+
},
|
|
42
|
+
required: ['query'],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'read_file',
|
|
47
|
+
description: 'Read a specific file from the AKB. Use after search_akb to read full content of interesting files.',
|
|
48
|
+
input_schema: {
|
|
49
|
+
type: 'object' as const,
|
|
50
|
+
properties: {
|
|
51
|
+
path: { type: 'string', description: 'File path relative to AKB root (e.g. "operations/decisions/008-repo-structure.md", "projects/projects.md")' },
|
|
52
|
+
},
|
|
53
|
+
required: ['path'],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'list_files',
|
|
58
|
+
description: 'List files in a directory. Useful to discover what exists (e.g. "operations/waves/", "roles/engineer/journal/").',
|
|
59
|
+
input_schema: {
|
|
60
|
+
type: 'object' as const,
|
|
61
|
+
properties: {
|
|
62
|
+
path: { type: 'string', description: 'Directory path relative to AKB root (e.g. "operations/standups", "roles/pm/journal")' },
|
|
63
|
+
pattern: { type: 'string', description: 'Glob pattern (default: "*.md")' },
|
|
64
|
+
},
|
|
65
|
+
required: ['path'],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
function executeAkbTool(name: string, input: Record<string, unknown>): string {
|
|
71
|
+
try {
|
|
72
|
+
switch (name) {
|
|
73
|
+
case 'search_akb': {
|
|
74
|
+
const query = String(input.query || '');
|
|
75
|
+
const searchPath = input.path ? String(input.path) : '';
|
|
76
|
+
const searchDir = path.resolve(COMPANY_ROOT, searchPath);
|
|
77
|
+
|
|
78
|
+
if (!fs.existsSync(searchDir)) return `Directory not found: ${searchPath || '/'}`;
|
|
79
|
+
|
|
80
|
+
// Find all .md files, then grep for query keywords
|
|
81
|
+
const mdFiles = glob.sync('**/*.md', { cwd: searchDir, nodir: true }).slice(0, 100);
|
|
82
|
+
const keywords = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
83
|
+
const results: string[] = [];
|
|
84
|
+
|
|
85
|
+
for (const file of mdFiles) {
|
|
86
|
+
const fullPath = path.join(searchDir, file);
|
|
87
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
88
|
+
const lower = content.toLowerCase();
|
|
89
|
+
const matchCount = keywords.filter(k => lower.includes(k)).length;
|
|
90
|
+
if (matchCount >= Math.max(1, Math.ceil(keywords.length * 0.5))) {
|
|
91
|
+
// Extract a relevant snippet (first matching line + context)
|
|
92
|
+
const lines = content.split('\n');
|
|
93
|
+
let snippet = '';
|
|
94
|
+
for (let i = 0; i < lines.length; i++) {
|
|
95
|
+
const ll = lines[i].toLowerCase();
|
|
96
|
+
if (keywords.some(k => ll.includes(k))) {
|
|
97
|
+
snippet = lines.slice(Math.max(0, i - 1), i + 3).join('\n').slice(0, 200);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const relPath = searchPath ? `${searchPath}/${file}` : file;
|
|
102
|
+
results.push(`📄 ${relPath} (${matchCount}/${keywords.length} keywords)\n${snippet}`);
|
|
103
|
+
}
|
|
104
|
+
if (results.length >= 8) break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return results.length > 0
|
|
108
|
+
? results.join('\n\n')
|
|
109
|
+
: `No results for "${query}" in ${searchPath || 'AKB'}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case 'read_file': {
|
|
113
|
+
const filePath = String(input.path || '');
|
|
114
|
+
const absolute = path.resolve(COMPANY_ROOT, filePath);
|
|
115
|
+
if (!fs.existsSync(absolute)) return `File not found: ${filePath}`;
|
|
116
|
+
const content = fs.readFileSync(absolute, 'utf-8');
|
|
117
|
+
return content.length > MAX_FILE_CHARS
|
|
118
|
+
? content.slice(0, MAX_FILE_CHARS) + `\n\n... (truncated, ${content.length} chars total)`
|
|
119
|
+
: content;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case 'list_files': {
|
|
123
|
+
const dirPath = String(input.path || '');
|
|
124
|
+
const pat = String(input.pattern || '*.md');
|
|
125
|
+
const absolute = path.resolve(COMPANY_ROOT, dirPath);
|
|
126
|
+
if (!fs.existsSync(absolute)) return `Directory not found: ${dirPath}`;
|
|
127
|
+
const files = glob.sync(pat, { cwd: absolute, nodir: true }).sort();
|
|
128
|
+
return files.length > 0
|
|
129
|
+
? files.map(f => `- ${dirPath}/${f}`).join('\n')
|
|
130
|
+
: `No files matching "${pat}" in ${dirPath}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
default:
|
|
134
|
+
return `Unknown tool: ${name}`;
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Run mini agent loop: LLM call → tool use → LLM call → ... → final text.
|
|
143
|
+
* Max MAX_TOOL_ROUNDS rounds of tool use, then force a text response.
|
|
144
|
+
*/
|
|
145
|
+
async function chatWithTools(
|
|
146
|
+
provider: LLMProvider,
|
|
147
|
+
systemPrompt: string,
|
|
148
|
+
initialMessages: LLMMessage[],
|
|
149
|
+
useTools: boolean,
|
|
150
|
+
): Promise<{ text: string; totalUsage: { inputTokens: number; outputTokens: number } }> {
|
|
151
|
+
const messages: LLMMessage[] = [...initialMessages];
|
|
152
|
+
const totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
153
|
+
const tools = useTools ? AKB_TOOLS : undefined;
|
|
154
|
+
|
|
155
|
+
for (let round = 0; round <= MAX_TOOL_ROUNDS; round++) {
|
|
156
|
+
const response = await provider.chat(systemPrompt, messages, tools);
|
|
157
|
+
totalUsage.inputTokens += response.usage.inputTokens;
|
|
158
|
+
totalUsage.outputTokens += response.usage.outputTokens;
|
|
159
|
+
|
|
160
|
+
// Check if there are tool calls
|
|
161
|
+
const toolCalls = response.content.filter(c => c.type === 'tool_use');
|
|
162
|
+
const textParts = response.content.filter(c => c.type === 'text').map(c => (c as { type: 'text'; text: string }).text);
|
|
163
|
+
|
|
164
|
+
if (toolCalls.length === 0 || round === MAX_TOOL_ROUNDS) {
|
|
165
|
+
// No tool calls or max rounds reached — return text
|
|
166
|
+
return { text: textParts.join('').trim(), totalUsage };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Execute tool calls and build tool results
|
|
170
|
+
messages.push({ role: 'assistant', content: response.content });
|
|
171
|
+
|
|
172
|
+
const toolResults: MessageContent[] = toolCalls.map(tc => {
|
|
173
|
+
const call = tc as { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> };
|
|
174
|
+
const result = executeAkbTool(call.name, call.input);
|
|
175
|
+
return { type: 'tool_result' as any, tool_use_id: call.id, content: result } as any;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
messages.push({ role: 'user', content: toolResults });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { text: '', totalUsage };
|
|
182
|
+
}
|
|
183
|
+
|
|
21
184
|
/**
|
|
22
185
|
* Build a compact company context for chat system prompts.
|
|
23
|
-
*
|
|
24
|
-
* Kept brief to minimize Haiku token cost.
|
|
186
|
+
* Provides a seed overview — the agent can dig deeper via AKB tools.
|
|
25
187
|
*/
|
|
26
188
|
function buildCompanyContext(): string {
|
|
27
189
|
const parts: string[] = [];
|
|
@@ -316,10 +478,27 @@ ${relContext}
|
|
|
316
478
|
|
|
317
479
|
${roleStyle}
|
|
318
480
|
|
|
481
|
+
AKB EXPLORATION (IMPORTANT):
|
|
482
|
+
You have tools to search and read the company knowledge base (AKB). Use them to ground your conversation in REAL company context.
|
|
483
|
+
Before responding, consider: "Is there something in our AKB that relates to this conversation?"
|
|
484
|
+
- search_akb: Search for keywords across the AKB (decisions, projects, journals, waves, standups)
|
|
485
|
+
- read_file: Read a specific file for details
|
|
486
|
+
- list_files: Discover what files exist in a directory
|
|
487
|
+
|
|
488
|
+
Useful paths to explore:
|
|
489
|
+
- operations/decisions/ — CEO decisions (what was decided and why)
|
|
490
|
+
- operations/waves/ — Work dispatches (what CEO asked teams to do)
|
|
491
|
+
- operations/standups/ — Daily standups (what everyone reported)
|
|
492
|
+
- projects/ — Active projects and their tasks
|
|
493
|
+
- roles/${roleId}/journal/ — Your own work journal
|
|
494
|
+
- knowledge/ — Domain knowledge
|
|
495
|
+
|
|
496
|
+
You don't need to search every time. But when the conversation touches on company work, decisions, or direction, DO search to find real facts rather than making up generic discussion.
|
|
497
|
+
|
|
319
498
|
CONVERSATION RULES:
|
|
320
499
|
1. Stay deeply in character — your expertise, vocabulary, and concerns should be DISTINCT from other roles.
|
|
321
500
|
2. Keep it to 1-3 sentences. No walls of text.
|
|
322
|
-
3. Be SPECIFIC. Reference actual projects, files, tools, metrics, or decisions — never vague platitudes.
|
|
501
|
+
3. Be SPECIFIC. Reference actual projects, files, tools, metrics, or decisions from the AKB — never vague platitudes.
|
|
323
502
|
4. Do NOT just agree with everyone. Real teams have different perspectives:
|
|
324
503
|
- If you genuinely disagree, say so (respectfully but firmly)
|
|
325
504
|
- If someone oversimplifies your domain, push back with specifics
|
|
@@ -340,39 +519,40 @@ ANTI-PATTERNS (never do these):
|
|
|
340
519
|
- Using the same emoji pattern as the previous speaker
|
|
341
520
|
- Restating the consensus without adding anything new
|
|
342
521
|
- Meta-commentary about the conversation itself ("wow we actually agreed")
|
|
343
|
-
- Generic statements that any role could say — speak from YOUR expertise
|
|
522
|
+
- Generic statements that any role could say — speak from YOUR expertise
|
|
523
|
+
- Talking about vague "refactoring" or "metrics" without referencing actual company work`;
|
|
344
524
|
|
|
345
525
|
const provider = getLLM();
|
|
346
|
-
|
|
526
|
+
|
|
527
|
+
// Use tool-based agent loop (AnthropicProvider supports tools; ClaudeCliProvider falls back to no-tools)
|
|
528
|
+
const useTools = provider instanceof AnthropicProvider;
|
|
529
|
+
const { text: raw, totalUsage } = await chatWithTools(
|
|
530
|
+
provider,
|
|
347
531
|
systemPrompt,
|
|
348
532
|
[{ role: 'user', content: historyText }],
|
|
533
|
+
useTools,
|
|
349
534
|
);
|
|
350
535
|
|
|
351
|
-
const
|
|
352
|
-
.filter(c => c.type === 'text')
|
|
353
|
-
.map(c => (c as { type: 'text'; text: string }).text)
|
|
354
|
-
.join('')
|
|
355
|
-
.trim()
|
|
356
|
-
.replace(/^["']|["']$/g, '');
|
|
536
|
+
const cleaned = raw.replace(/^["']|["']$/g, '');
|
|
357
537
|
|
|
358
538
|
// Filter out CLI noise and [SILENT]
|
|
359
|
-
const message = (
|
|
539
|
+
const message = (cleaned === '[SILENT]' || cleaned.startsWith('Error: Reached max turns') || !cleaned) ? '' : cleaned;
|
|
360
540
|
|
|
361
541
|
// Record usage in token ledger (category: chat)
|
|
362
|
-
if (
|
|
542
|
+
if (totalUsage) {
|
|
363
543
|
getLedger().record({
|
|
364
544
|
ts: new Date().toISOString(),
|
|
365
545
|
jobId: `chat-${channelId}`,
|
|
366
546
|
roleId,
|
|
367
547
|
model: process.env.SPEECH_MODEL || 'claude-haiku-4-5-20251001',
|
|
368
|
-
inputTokens:
|
|
369
|
-
outputTokens:
|
|
548
|
+
inputTokens: totalUsage.inputTokens ?? 0,
|
|
549
|
+
outputTokens: totalUsage.outputTokens ?? 0,
|
|
370
550
|
});
|
|
371
551
|
}
|
|
372
552
|
|
|
373
553
|
res.json({
|
|
374
554
|
message,
|
|
375
|
-
tokens:
|
|
555
|
+
tokens: totalUsage,
|
|
376
556
|
});
|
|
377
557
|
} catch (err) {
|
|
378
558
|
next(err);
|