osborn 0.9.189 → 0.9.191
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/dist/claude-llm.js +8 -3
- package/dist/config.d.ts +1 -1
- package/dist/config.js +11 -3
- package/dist/index.js +37 -6
- package/dist/turn-detector-shim.d.ts +10 -4
- package/dist/turn-detector-shim.js +92 -17
- package/package.json +1 -1
package/dist/claude-llm.js
CHANGED
|
@@ -62,6 +62,11 @@ function stripMarkdownForTTS(text) {
|
|
|
62
62
|
.replace(/ +/g, ' ')
|
|
63
63
|
.trim();
|
|
64
64
|
}
|
|
65
|
+
function getSubagentsDir(workingDir) {
|
|
66
|
+
const dir = join(workingDir, 'subagents');
|
|
67
|
+
mkdirSync(dir, { recursive: true });
|
|
68
|
+
return dir;
|
|
69
|
+
}
|
|
65
70
|
/**
|
|
66
71
|
* Load skill files from agent/.claude/skills/{name}/SKILL.md
|
|
67
72
|
* Injects into system prompt so Claude sees them as available capabilities.
|
|
@@ -1332,7 +1337,7 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1332
1337
|
// agents roster prevents any SubagentStop(agent_type==='writer') from
|
|
1333
1338
|
// firing inside this one-shot query and re-arming the backstop loop.
|
|
1334
1339
|
const reviewerOptions = {
|
|
1335
|
-
cwd: this.#opts.workingDirectory,
|
|
1340
|
+
cwd: getSubagentsDir(this.#opts.workingDirectory),
|
|
1336
1341
|
permissionMode: 'default',
|
|
1337
1342
|
systemPrompt: NAMED_AGENTS.reviewer.prompt,
|
|
1338
1343
|
allowedTools: ['Read', 'Glob', 'Grep', 'Bash', 'Write', 'Edit'],
|
|
@@ -1441,7 +1446,7 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1441
1446
|
gitDiff,
|
|
1442
1447
|
].join('\n');
|
|
1443
1448
|
const testerOptions = {
|
|
1444
|
-
cwd: this.#opts.workingDirectory,
|
|
1449
|
+
cwd: getSubagentsDir(this.#opts.workingDirectory),
|
|
1445
1450
|
permissionMode: 'default',
|
|
1446
1451
|
systemPrompt: NAMED_AGENTS.tester.prompt,
|
|
1447
1452
|
allowedTools: ['Read', 'Glob', 'Grep', 'Bash', 'Write', 'Edit'],
|
|
@@ -1528,7 +1533,7 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1528
1533
|
// an agents roster would allow delegation back to the writer, which would
|
|
1529
1534
|
// fire SubagentStop(agent_type==='writer') and re-arm the backstop.
|
|
1530
1535
|
const gateOptions = {
|
|
1531
|
-
cwd: this.#opts.workingDirectory,
|
|
1536
|
+
cwd: getSubagentsDir(this.#opts.workingDirectory),
|
|
1532
1537
|
permissionMode: 'default',
|
|
1533
1538
|
systemPrompt: NAMED_AGENTS.reasoner.prompt,
|
|
1534
1539
|
hooks: {
|
package/dist/config.d.ts
CHANGED
|
@@ -146,7 +146,7 @@ export declare function invalidateSessionListCache(): void;
|
|
|
146
146
|
*
|
|
147
147
|
* @param limit - Max sessions to return (default 100, sorted by recency)
|
|
148
148
|
*/
|
|
149
|
-
export declare function listAllClaudeSessions(limit?: number): Promise<ClaudeSessionEntry[]>;
|
|
149
|
+
export declare function listAllClaudeSessions(limit?: number, includeSubagents?: boolean): Promise<ClaudeSessionEntry[]>;
|
|
150
150
|
/**
|
|
151
151
|
* Session summary for context briefing when switching sessions
|
|
152
152
|
*/
|
package/dist/config.js
CHANGED
|
@@ -555,15 +555,23 @@ export function invalidateSessionListCache() {
|
|
|
555
555
|
*
|
|
556
556
|
* @param limit - Max sessions to return (default 100, sorted by recency)
|
|
557
557
|
*/
|
|
558
|
-
export async function listAllClaudeSessions(limit = 1000) {
|
|
558
|
+
export async function listAllClaudeSessions(limit = 1000, includeSubagents = false) {
|
|
559
559
|
if (_sessionListCache && Date.now() < _sessionListCache.expiresAt) {
|
|
560
|
-
|
|
560
|
+
const cached = includeSubagents
|
|
561
|
+
? _sessionListCache.data
|
|
562
|
+
: _sessionListCache.data.filter(s => !s.projectSlug.endsWith('-subagents'));
|
|
563
|
+
return cached.slice(0, limit);
|
|
561
564
|
}
|
|
562
565
|
const projectsDir = getClaudeProjectsDir();
|
|
563
566
|
if (!existsSync(projectsDir))
|
|
564
567
|
return [];
|
|
565
|
-
// 1. Discover all project folders
|
|
568
|
+
// 1. Discover all project folders — skip subagent slugs unless explicitly requested.
|
|
569
|
+
// Subagent sessions live in {workingDir}/subagents, which slugifies to a folder
|
|
570
|
+
// ending in "-subagents". Filtering here keeps the session limit budget for
|
|
571
|
+
// real user sessions and prevents subagent noise from crowding out other projects.
|
|
566
572
|
const projectFolders = readdirSync(projectsDir).filter(name => {
|
|
573
|
+
if (!includeSubagents && name.endsWith('-subagents'))
|
|
574
|
+
return false;
|
|
567
575
|
const fullPath = join(projectsDir, name);
|
|
568
576
|
try {
|
|
569
577
|
return statSync(fullPath).isDirectory();
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,7 @@ import { createGunzip } from 'node:zlib';
|
|
|
29
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
30
30
|
const __dirname = dirname(__filename);
|
|
31
31
|
import { createPatch } from 'diff';
|
|
32
|
-
import { loadConfig, getMcpServers, getEnabledMcpServerNames, getVoiceMode, getDirectConfig, listAllClaudeSessions, invalidateSessionListCache, getMostRecentSessionId, sessionExists, getSessionSummary, getConversationHistory, ensureSessionWorkspace, getMcpServerStatusList, buildMcpServersForKeys, listWorkspaceArtifacts } from './config.js';
|
|
32
|
+
import { loadConfig, getMcpServers, getEnabledMcpServerNames, getVoiceMode, getDirectConfig, listSessions, listAllClaudeSessions, invalidateSessionListCache, getMostRecentSessionId, sessionExists, getSessionSummary, getConversationHistory, ensureSessionWorkspace, getMcpServerStatusList, buildMcpServersForKeys, listWorkspaceArtifacts } from './config.js';
|
|
33
33
|
import { createSTT, createTTS, DIRECT_MODE_STT, DIRECT_MODE_TTS } from './voice-io.js';
|
|
34
34
|
import { createClaudeLLM, NAMED_AGENTS, applyTurbo } from './claude-llm.js';
|
|
35
35
|
import { clearPipelineFastBrainSession, prewarmBM25Index } from './pipeline-fastbrain.js';
|
|
@@ -3603,9 +3603,19 @@ async function main() {
|
|
|
3603
3603
|
// discardAudioIfUninterruptible: true, ttsReadIdleTimeout: 10000,
|
|
3604
3604
|
// maxUnrecoverableErrors: 3) are what was silently running via caret-resolved
|
|
3605
3605
|
// 1.4.5 throughout the user's working month. Restoring them.
|
|
3606
|
-
|
|
3606
|
+
// STT endpointing is the default (nova-3, 25ms silence-based VAD — reliable, fast).
|
|
3607
|
+
// CloudTurnDetector (ML semantic EOT) is used ONLY if LIVEKIT_REMOTE_EOT_URL is set
|
|
3608
|
+
// AND the endpoint passes a startup probe (returns valid JSON probability).
|
|
3609
|
+
// If the probe fails (wrong URL, no auth, non-JSON response), STT is kept.
|
|
3610
|
+
let turnDetection = 'stt';
|
|
3611
|
+
if (process.env.LIVEKIT_REMOTE_EOT_URL) {
|
|
3612
|
+
const detector = new CloudTurnDetector();
|
|
3613
|
+
const eotLive = await detector.probe();
|
|
3614
|
+
if (eotLive)
|
|
3615
|
+
turnDetection = detector;
|
|
3616
|
+
}
|
|
3607
3617
|
const session = new voice.AgentSession({
|
|
3608
|
-
turnDetection
|
|
3618
|
+
turnDetection,
|
|
3609
3619
|
preemptiveGeneration: false, // Only fire LLM on final committed transcript, not partial preemptives
|
|
3610
3620
|
// Commented out — kept for reference. These were added across 0.9.60/0.9.61
|
|
3611
3621
|
// to try to harden interrupt + TTS handling, but evidence (osbornojure
|
|
@@ -4553,11 +4563,32 @@ async function main() {
|
|
|
4553
4563
|
console.log('💓 Sending agent_ready signal...');
|
|
4554
4564
|
let readySent = false;
|
|
4555
4565
|
// (realtime provider removed)
|
|
4556
|
-
// Session list state — populated
|
|
4566
|
+
// Session list state — populated in two tiers (fast then full)
|
|
4557
4567
|
let sessionsForFrontend = [];
|
|
4558
4568
|
let recentSessionId = null;
|
|
4559
4569
|
let hasRecentSession = false;
|
|
4560
4570
|
let sessionsLoaded = false;
|
|
4571
|
+
// Tier 1: fast scan — current project only, ~5ms
|
|
4572
|
+
// Gives the frontend enough to show "Resume last session?" immediately.
|
|
4573
|
+
try {
|
|
4574
|
+
const recentSessions = await listSessions(workingDir);
|
|
4575
|
+
if (recentSessions.length > 0) {
|
|
4576
|
+
const top = recentSessions[0];
|
|
4577
|
+
recentSessionId = top.sessionId;
|
|
4578
|
+
hasRecentSession = true;
|
|
4579
|
+
sessionsForFrontend = [{
|
|
4580
|
+
sessionId: top.sessionId,
|
|
4581
|
+
projectSlug: top.projectPath,
|
|
4582
|
+
projectPath: top.projectPath,
|
|
4583
|
+
cwd: top.projectPath,
|
|
4584
|
+
timestamp: top.timestamp.toISOString(),
|
|
4585
|
+
lastMessage: top.lastMessage,
|
|
4586
|
+
messageCount: top.messageCount,
|
|
4587
|
+
fileSize: 0,
|
|
4588
|
+
}];
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
catch { }
|
|
4561
4592
|
const sendReady = async () => {
|
|
4562
4593
|
if (readySent)
|
|
4563
4594
|
return;
|
|
@@ -4580,13 +4611,13 @@ async function main() {
|
|
|
4580
4611
|
});
|
|
4581
4612
|
};
|
|
4582
4613
|
const readyInterval = setInterval(sendReady, 2000);
|
|
4583
|
-
// Fire immediately
|
|
4614
|
+
// Fire immediately with the fast-scanned recent session (sessionsLoaded still false)
|
|
4584
4615
|
await sendReady();
|
|
4585
4616
|
setTimeout(() => {
|
|
4586
4617
|
clearInterval(readyInterval);
|
|
4587
4618
|
console.log('✅ agent_ready retries complete');
|
|
4588
4619
|
}, 20000);
|
|
4589
|
-
//
|
|
4620
|
+
// Tier 2: full cross-project scan in background — no blocking
|
|
4590
4621
|
listAllClaudeSessions().then(allSessions => {
|
|
4591
4622
|
recentSessionId = allSessions.length > 0 ? allSessions[0].sessionId : null;
|
|
4592
4623
|
hasRecentSession = allSessions.length > 0;
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
* because it calls getJobContext(). This shim implements the same _TurnDetector
|
|
6
6
|
* interface but makes the remote HTTP call directly — no worker framework needed.
|
|
7
7
|
*
|
|
8
|
+
* Auth: LiveKit Cloud inference requires a signed JWT (same format as room access
|
|
9
|
+
* tokens). We cache the token and refresh it before expiry so we're not signing
|
|
10
|
+
* on every turn.
|
|
11
|
+
*
|
|
8
12
|
* On LiveKit Cloud (LIVEKIT_REMOTE_EOT_URL set): HTTP call to inference gateway.
|
|
9
13
|
* Without the URL: Returns 1.0 (always end of turn — STT endpointing handles it).
|
|
10
14
|
*/
|
|
11
15
|
import type { llm } from '@livekit/agents';
|
|
12
|
-
/**
|
|
13
|
-
* Implements _TurnDetector interface for LiveKit Cloud remote inference
|
|
14
|
-
* without requiring JobContext / worker framework.
|
|
15
|
-
*/
|
|
16
16
|
export declare class CloudTurnDetector {
|
|
17
17
|
#private;
|
|
18
18
|
readonly model = "lk_end_of_utterance_multilingual";
|
|
@@ -20,5 +20,11 @@ export declare class CloudTurnDetector {
|
|
|
20
20
|
constructor();
|
|
21
21
|
unlikelyThreshold(_language?: string): Promise<number | undefined>;
|
|
22
22
|
supportsLanguage(_language?: string): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Startup probe — send a minimal request to confirm the endpoint returns
|
|
25
|
+
* valid JSON probability. Returns true if the EOT service is live and real.
|
|
26
|
+
* Called once at session init; if false, index.ts falls back to 'stt'.
|
|
27
|
+
*/
|
|
28
|
+
probe(): Promise<boolean>;
|
|
23
29
|
predictEndOfTurn(chatCtx: llm.ChatContext, _timeout?: number): Promise<number>;
|
|
24
30
|
}
|
|
@@ -5,19 +5,24 @@
|
|
|
5
5
|
* because it calls getJobContext(). This shim implements the same _TurnDetector
|
|
6
6
|
* interface but makes the remote HTTP call directly — no worker framework needed.
|
|
7
7
|
*
|
|
8
|
+
* Auth: LiveKit Cloud inference requires a signed JWT (same format as room access
|
|
9
|
+
* tokens). We cache the token and refresh it before expiry so we're not signing
|
|
10
|
+
* on every turn.
|
|
11
|
+
*
|
|
8
12
|
* On LiveKit Cloud (LIVEKIT_REMOTE_EOT_URL set): HTTP call to inference gateway.
|
|
9
13
|
* Without the URL: Returns 1.0 (always end of turn — STT endpointing handles it).
|
|
10
14
|
*/
|
|
11
15
|
import { log } from '@livekit/agents';
|
|
16
|
+
import { AccessToken } from 'livekit-server-sdk';
|
|
12
17
|
const REMOTE_INFERENCE_TIMEOUT = 2000;
|
|
13
18
|
const MAX_HISTORY_TURNS = 15;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* without requiring JobContext / worker framework.
|
|
17
|
-
*/
|
|
19
|
+
const TOKEN_TTL_SECONDS = 600; // 10-minute JWT
|
|
20
|
+
const TOKEN_REFRESH_BUFFER = 60; // refresh 60s before expiry
|
|
18
21
|
export class CloudTurnDetector {
|
|
19
22
|
#remoteUrl;
|
|
20
23
|
#logger = log();
|
|
24
|
+
#cachedToken;
|
|
25
|
+
#tokenExpiresAt = 0;
|
|
21
26
|
model = 'lk_end_of_utterance_multilingual';
|
|
22
27
|
provider = 'livekit';
|
|
23
28
|
constructor() {
|
|
@@ -30,18 +35,78 @@ export class CloudTurnDetector {
|
|
|
30
35
|
console.log(`🧠 Turn detector: LiveKit Cloud remote inference (${this.#remoteUrl})`);
|
|
31
36
|
}
|
|
32
37
|
else {
|
|
33
|
-
console.log('🧠 Turn detector: No LIVEKIT_REMOTE_EOT_URL — STT endpointing
|
|
38
|
+
console.log('🧠 Turn detector: No LIVEKIT_REMOTE_EOT_URL — STT endpointing only');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async #getAuthToken() {
|
|
42
|
+
const apiKey = process.env.LIVEKIT_API_KEY;
|
|
43
|
+
const apiSecret = process.env.LIVEKIT_API_SECRET;
|
|
44
|
+
if (!apiKey || !apiSecret)
|
|
45
|
+
return undefined;
|
|
46
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
47
|
+
if (this.#cachedToken && nowSec < this.#tokenExpiresAt - TOKEN_REFRESH_BUFFER) {
|
|
48
|
+
return this.#cachedToken;
|
|
34
49
|
}
|
|
50
|
+
// Sign a fresh token — identity is the agent, no room grants needed for inference
|
|
51
|
+
const at = new AccessToken(apiKey, apiSecret, {
|
|
52
|
+
identity: 'osborn-eot-agent',
|
|
53
|
+
ttl: `${TOKEN_TTL_SECONDS}s`,
|
|
54
|
+
});
|
|
55
|
+
this.#cachedToken = await at.toJwt();
|
|
56
|
+
this.#tokenExpiresAt = nowSec + TOKEN_TTL_SECONDS;
|
|
57
|
+
return this.#cachedToken;
|
|
35
58
|
}
|
|
36
59
|
async unlikelyThreshold(_language) {
|
|
37
|
-
return undefined;
|
|
60
|
+
return undefined;
|
|
38
61
|
}
|
|
39
62
|
async supportsLanguage(_language) {
|
|
40
|
-
return true;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Startup probe — send a minimal request to confirm the endpoint returns
|
|
67
|
+
* valid JSON probability. Returns true if the EOT service is live and real.
|
|
68
|
+
* Called once at session init; if false, index.ts falls back to 'stt'.
|
|
69
|
+
*/
|
|
70
|
+
async probe() {
|
|
71
|
+
if (!this.#remoteUrl)
|
|
72
|
+
return false;
|
|
73
|
+
try {
|
|
74
|
+
const token = await this.#getAuthToken();
|
|
75
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
76
|
+
if (token)
|
|
77
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
78
|
+
const resp = await fetch(`${this.#remoteUrl}/eot/multi`, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
messages: [{ role: 'user', content: 'test' }],
|
|
82
|
+
jobId: 'osborn-probe',
|
|
83
|
+
workerId: 'osborn-direct',
|
|
84
|
+
}),
|
|
85
|
+
headers,
|
|
86
|
+
signal: AbortSignal.timeout(3000),
|
|
87
|
+
});
|
|
88
|
+
if (!resp.ok)
|
|
89
|
+
return false;
|
|
90
|
+
const text = await resp.text();
|
|
91
|
+
try {
|
|
92
|
+
const data = JSON.parse(text);
|
|
93
|
+
const ok = typeof data.probability === 'number';
|
|
94
|
+
console.log(`🧠 EOT probe: ${ok ? '✅ live' : '❌ non-JSON ("' + text.slice(0, 30) + '")'} — ${ok ? 'using CloudTurnDetector' : 'falling back to STT'}`);
|
|
95
|
+
return ok;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
console.log(`🧠 EOT probe: ❌ non-JSON response ("${text.slice(0, 40)}") — falling back to STT endpointing`);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
console.log(`🧠 EOT probe: ❌ unreachable — ${err instanceof Error ? err.message : err} — falling back to STT`);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
41
106
|
}
|
|
42
107
|
async predictEndOfTurn(chatCtx, _timeout) {
|
|
43
108
|
if (!this.#remoteUrl) {
|
|
44
|
-
return 1.0;
|
|
109
|
+
return 1.0;
|
|
45
110
|
}
|
|
46
111
|
try {
|
|
47
112
|
const messages = chatCtx
|
|
@@ -57,31 +122,41 @@ export class CloudTurnDetector {
|
|
|
57
122
|
excludeAudio: true,
|
|
58
123
|
excludeTimestamp: true,
|
|
59
124
|
}),
|
|
60
|
-
// Dummy IDs — LiveKit Cloud uses these for routing/logging, not auth
|
|
61
125
|
jobId: `osborn-${Date.now()}`,
|
|
62
126
|
workerId: 'osborn-direct',
|
|
63
127
|
};
|
|
64
128
|
const agentId = process.env.LIVEKIT_AGENT_ID;
|
|
65
|
-
if (agentId)
|
|
129
|
+
if (agentId)
|
|
66
130
|
request.agentId = agentId;
|
|
67
|
-
|
|
131
|
+
const token = await this.#getAuthToken();
|
|
132
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
133
|
+
if (token)
|
|
134
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
68
135
|
const resp = await fetch(`${this.#remoteUrl}/eot/multi`, {
|
|
69
136
|
method: 'POST',
|
|
70
137
|
body: JSON.stringify(request),
|
|
71
|
-
headers
|
|
138
|
+
headers,
|
|
72
139
|
signal: AbortSignal.timeout(REMOTE_INFERENCE_TIMEOUT),
|
|
73
140
|
});
|
|
74
141
|
if (!resp.ok) {
|
|
75
|
-
|
|
142
|
+
this.#logger.warn(`EOT inference returned ${resp.status} — falling back to STT`);
|
|
143
|
+
return 1.0;
|
|
76
144
|
}
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
145
|
+
const text = await resp.text();
|
|
146
|
+
try {
|
|
147
|
+
const data = JSON.parse(text);
|
|
148
|
+
if (typeof data.probability === 'number' && data.probability >= 0) {
|
|
149
|
+
return data.probability;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Non-JSON response (e.g. "OK") — log once then fall through
|
|
154
|
+
this.#logger.warn(`EOT inference returned non-JSON: "${text.slice(0, 40)}" — auth may be wrong`);
|
|
80
155
|
}
|
|
81
156
|
return 1.0;
|
|
82
157
|
}
|
|
83
158
|
catch {
|
|
84
|
-
return 1.0;
|
|
159
|
+
return 1.0;
|
|
85
160
|
}
|
|
86
161
|
}
|
|
87
162
|
}
|