kira-glasses-bridge 2.32.0 → 2.34.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/package.json +1 -1
- package/src/agents/claude-calls.mjs +1 -0
- package/src/agents/claude.mjs +17 -1
- package/src/agents/codex-items.mjs +2 -0
- package/src/agents/codex-subagents.mjs +60 -0
- package/src/agents/codex.mjs +13 -3
- package/src/agents/protocol.mjs +6 -0
- package/src/agents/subagents.mjs +121 -0
package/package.json
CHANGED
|
@@ -58,6 +58,7 @@ export function describeAction(name, input, cwd) {
|
|
|
58
58
|
return act('fetch', 'Fetch', short(i.url, 200))
|
|
59
59
|
case 'WebSearch':
|
|
60
60
|
return act('fetch', 'Search the web', short(i.query, 160))
|
|
61
|
+
case 'Agent':
|
|
61
62
|
case 'Task':
|
|
62
63
|
return act('delegate', 'Subtask', short(i.description ?? i.prompt, 160))
|
|
63
64
|
case 'TodoWrite':
|
package/src/agents/claude.mjs
CHANGED
|
@@ -15,6 +15,8 @@ import { planLimits } from './claude-local.mjs'
|
|
|
15
15
|
import { contextFrom, limitsFromEvent, rejectedWindow, stoppedBy } from './claude-usage.mjs'
|
|
16
16
|
import { sdkOptionsFor } from './settings.mjs'
|
|
17
17
|
import { kiraPluginOptions } from '../kira-plugin.mjs'
|
|
18
|
+
import { git } from '../git.mjs'
|
|
19
|
+
import { isAgentCall, subagentFeed } from './subagents.mjs'
|
|
18
20
|
|
|
19
21
|
/** Kira's MCP server as Claude Code connects to it: Streamable HTTP, the token as the bearer. */
|
|
20
22
|
const kiraServer = ({ url, token }) => ({ type: 'http', url, headers: { Authorization: `Bearer ${token}` } })
|
|
@@ -100,6 +102,9 @@ async function run({ prompt, sessionId, cwd, persona, settings, abort }, io) {
|
|
|
100
102
|
const env = { ...process.env, CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: '1', ENABLE_CLAUDEAI_MCP_SERVERS: 'false' }
|
|
101
103
|
/** Background tasks the model still managed to start (an older CLI ignoring the flag): tool_use id -> description. */
|
|
102
104
|
const orphans = new Map()
|
|
105
|
+
/** The agents it starts, each with where it works (subagents.mjs). */
|
|
106
|
+
const branchOf = async path => (await git(path, ['branch', '--show-current']))?.trim() || null
|
|
107
|
+
const agents = subagentFeed(e => io.emit(e), branchOf)
|
|
103
108
|
|
|
104
109
|
try {
|
|
105
110
|
const q = query({
|
|
@@ -144,6 +149,9 @@ async function run({ prompt, sessionId, cwd, persona, settings, abort }, io) {
|
|
|
144
149
|
if (m.is_backgrounded && m.tool_use_id) orphans.set(m.tool_use_id, m.description ?? m.subagent_type ?? 'task')
|
|
145
150
|
} else if (m.type === 'system' && m.subtype === 'task_notification') {
|
|
146
151
|
if (m.tool_use_id) orphans.delete(m.tool_use_id)
|
|
152
|
+
if (m.tool_use_id) agents.ended(m.tool_use_id, m.status, m.summary)
|
|
153
|
+
} else if (m.type === 'system' && m.subtype === 'task_progress') {
|
|
154
|
+
if (m.tool_use_id) agents.progress(m.tool_use_id, m.summary)
|
|
147
155
|
} else if (m.type === 'assistant') {
|
|
148
156
|
// A new turn of the main thread: how full the context is now, as /context counts it (no model call).
|
|
149
157
|
if (!nested && m.message?.id && m.message.id !== turn) {
|
|
@@ -153,7 +161,13 @@ async function run({ prompt, sessionId, cwd, persona, settings, abort }, io) {
|
|
|
153
161
|
for (const b of m.message?.content ?? []) {
|
|
154
162
|
// A subagent's narration is for its parent, not the user; its actions still show what is going on while the Subtask runs.
|
|
155
163
|
if (b.type === 'text' && b.text?.trim() && !nested) io.emit({ kind: 'message', text: short(b.text, TEXT_CAP) })
|
|
156
|
-
else if (b.type === 'tool_use')
|
|
164
|
+
else if (b.type === 'tool_use') {
|
|
165
|
+
// A step inside an agent says whose it is.
|
|
166
|
+
const whose = nested ? { agent: m.parent_tool_use_id } : {}
|
|
167
|
+
io.emit({ kind: 'action', id: b.id, tool: b.name, ...describeAction(b.name, b.input, cwd), ...whose })
|
|
168
|
+
if (isAgentCall(b.name)) agents.called(b.id, b.input, m.parent_tool_use_id)
|
|
169
|
+
if (nested) agents.touched(m.parent_tool_use_id, b.input)
|
|
170
|
+
}
|
|
157
171
|
}
|
|
158
172
|
} else if (m.type === 'user') {
|
|
159
173
|
for (const b of m.message?.content ?? []) {
|
|
@@ -161,10 +175,12 @@ async function run({ prompt, sessionId, cwd, persona, settings, abort }, io) {
|
|
|
161
175
|
const t = typeof b.content === 'string' ? b.content : Array.isArray(b.content) ? b.content.map(c => (c.type === 'text' ? c.text : '')).join('\n') : ''
|
|
162
176
|
if (/^Async agent launched successfully/.test(t.trim()) && !orphans.has(b.tool_use_id)) orphans.set(b.tool_use_id, 'subagent')
|
|
163
177
|
io.emit({ kind: 'action_result', id: b.tool_use_id, ok: !b.is_error, preview: short(t.trim(), 240) })
|
|
178
|
+
if (agents.has(b.tool_use_id)) agents.returned(b.tool_use_id, m.tool_use_result, b.is_error)
|
|
164
179
|
}
|
|
165
180
|
}
|
|
166
181
|
} else if (m.type === 'result') {
|
|
167
182
|
session = m.session_id ?? session
|
|
183
|
+
agents.stopAll()
|
|
168
184
|
// A turn that ended on an API error (a usage limit among them) is a success carrying the error.
|
|
169
185
|
const limit = { limits: usage.known, rejected, limitPrefixes: USAGE_LIMIT_ERROR_PREFIXES }
|
|
170
186
|
if (m.subtype === 'success' && !m.is_error) text = String(m.result ?? '')
|
|
@@ -79,6 +79,8 @@ export function itemAction(item, cwd) {
|
|
|
79
79
|
case 'plan':
|
|
80
80
|
return act('plan', 'Plan', short(item.text, 160), { tool: 'Plan' })
|
|
81
81
|
case 'collabAgentToolCall':
|
|
82
|
+
// Spawning one is a step; waiting on or messaging its agents moves the agent's row (codex-subagents.mjs).
|
|
83
|
+
if (item.tool !== 'spawnAgent') return null
|
|
82
84
|
return act('delegate', 'Subtask', short(item.prompt ?? '', 160), { tool: 'Subtask' })
|
|
83
85
|
case 'enteredReviewMode':
|
|
84
86
|
return act('read', 'Review', short(item.review ?? '', 160), { tool: 'Review' })
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// The agents Codex spawns inside a turn (its collab tools), as protocol `agent` events, the way
|
|
2
|
+
// subagents.mjs does Claude Code's. A spawn is a collabAgentToolCall naming the new agent's thread;
|
|
3
|
+
// that thread's items then arrive on the same app server, so each one says whose it is (`whose`), and
|
|
4
|
+
// later collab calls and subAgentActivity items say how each agent stands. Codex's agents work in the
|
|
5
|
+
// turn's own folder.
|
|
6
|
+
import { short } from './claude-calls.mjs'
|
|
7
|
+
|
|
8
|
+
const STATES = {
|
|
9
|
+
completed: 'done',
|
|
10
|
+
errored: 'failed',
|
|
11
|
+
interrupted: 'stopped',
|
|
12
|
+
shutdown: 'stopped',
|
|
13
|
+
notFound: 'stopped',
|
|
14
|
+
}
|
|
15
|
+
const ACTIVITY = { completed: 'done', interrupted: 'stopped' }
|
|
16
|
+
|
|
17
|
+
/** The last part of an agent's path ("/root/explorer" → "explorer"): the name Codex gave it. */
|
|
18
|
+
const nameOf = path => String(path ?? '').split('/').filter(Boolean).pop() || null
|
|
19
|
+
|
|
20
|
+
export function codexAgentFeed(emit) {
|
|
21
|
+
const agents = new Map()
|
|
22
|
+
/** Each agent's thread, to the spawn call that made it. */
|
|
23
|
+
const threads = new Map()
|
|
24
|
+
const send = a => {
|
|
25
|
+
agents.set(a.id, a)
|
|
26
|
+
emit(a)
|
|
27
|
+
}
|
|
28
|
+
const settle = (threadId, status, name) => {
|
|
29
|
+
const a = agents.get(threads.get(threadId))
|
|
30
|
+
if (!a) return
|
|
31
|
+
const named = name && !a.name ? { name } : {}
|
|
32
|
+
if (status && a.status === 'running') send({ ...a, status, ...named })
|
|
33
|
+
else if (named.name) send({ ...a, ...named })
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
/** The agent a thread's items belong to, if the thread is one Codex spawned. */
|
|
37
|
+
whose: threadId => threads.get(threadId) ?? null,
|
|
38
|
+
started(item) {
|
|
39
|
+
if (item.type !== 'collabAgentToolCall' || item.tool !== 'spawnAgent') return
|
|
40
|
+
const description = short(item.prompt ?? '', 200)
|
|
41
|
+
send({ kind: 'agent', id: item.id, type: 'agent', description, status: 'running' })
|
|
42
|
+
},
|
|
43
|
+
completed(item) {
|
|
44
|
+
if (item.type === 'subAgentActivity') {
|
|
45
|
+
return settle(item.agentThreadId, ACTIVITY[item.kind], nameOf(item.agentPath))
|
|
46
|
+
}
|
|
47
|
+
if (item.type !== 'collabAgentToolCall') return
|
|
48
|
+
if (item.tool === 'spawnAgent') {
|
|
49
|
+
for (const t of item.receiverThreadIds ?? []) threads.set(t, item.id)
|
|
50
|
+
const a = agents.get(item.id)
|
|
51
|
+
if (a && item.status === 'failed') send({ ...a, status: 'failed' })
|
|
52
|
+
}
|
|
53
|
+
for (const [t, state] of Object.entries(item.agentsStates ?? {})) settle(t, STATES[state?.status])
|
|
54
|
+
},
|
|
55
|
+
/** The turn ended: what still runs ended with it (`status`: done, or stopped when it was cut short). */
|
|
56
|
+
endAll(status) {
|
|
57
|
+
for (const a of agents.values()) if (a.status === 'running') send({ ...a, status })
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/agents/codex.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { codexBin, openCodex } from './codex-rpc.mjs'
|
|
|
9
9
|
import { codexInput } from './codex-input.mjs'
|
|
10
10
|
import { turnSteering } from './codex-steer.mjs'
|
|
11
11
|
import { codexStream } from './codex-live.mjs'
|
|
12
|
+
import { codexAgentFeed } from './codex-subagents.mjs'
|
|
12
13
|
import { liveEmitter } from './live.mjs'
|
|
13
14
|
import { kiraDecides } from './checks.mjs'
|
|
14
15
|
import { CODEX_ASKS, askPerson, refusedByPolicy, said } from './codex-ask.mjs'
|
|
@@ -51,6 +52,8 @@ async function run({ prompt, attachments, sessionId, cwd, persona, settings, abo
|
|
|
51
52
|
/** What is still being written (live.mjs): sent as it comes, the final form of each kept. */
|
|
52
53
|
const live = liveEmitter(io.emit)
|
|
53
54
|
const stream = codexStream()
|
|
55
|
+
/** The agents it spawns, and which thread is whose (codex-subagents.mjs). */
|
|
56
|
+
const agents = codexAgentFeed(e => io.emit(e))
|
|
54
57
|
let finished
|
|
55
58
|
const ended = new Promise(resolve => (finished = resolve))
|
|
56
59
|
// What the person says while a turn runs goes into it (`codex` is the app server, opened below).
|
|
@@ -86,16 +89,22 @@ async function run({ prompt, attachments, sessionId, cwd, persona, settings, abo
|
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
const onNotify = (method, p) => {
|
|
92
|
+
// An item from a thread Codex spawned is its agent's: its steps say so, and what it says is for Codex.
|
|
93
|
+
const whose = p?.threadId ? agents.whose(p.threadId) : null
|
|
89
94
|
const update = stream.update(method, p)
|
|
90
|
-
if (update) return live.update(update)
|
|
95
|
+
if (update) return whose ? undefined : live.update(update)
|
|
91
96
|
if (method === 'item/started') {
|
|
92
97
|
if (p.item.type === 'fileChange') changes.set(p.item.id, p.item.changes ?? [])
|
|
98
|
+
agents.started(p.item)
|
|
93
99
|
const action = itemAction(p.item, cwd)
|
|
94
|
-
if (action) io.emit({ kind: 'action', id: p.item.id, ...action })
|
|
100
|
+
if (action) io.emit({ kind: 'action', id: p.item.id, ...action, ...(whose ? { agent: whose } : {}) })
|
|
95
101
|
} else if (method === 'item/completed') {
|
|
102
|
+
agents.completed(p.item)
|
|
96
103
|
const reviewed = review.completed(p.item)
|
|
97
104
|
if (reviewed) io.emit(reviewed)
|
|
98
|
-
if (p.item.type === 'agentMessage'
|
|
105
|
+
if (whose && p.item.type === 'agentMessage') {
|
|
106
|
+
// Its agent's answer goes to Codex, not the person.
|
|
107
|
+
} else if (p.item.type === 'agentMessage' && p.item.text?.trim()) {
|
|
99
108
|
text = p.item.text
|
|
100
109
|
if (!review.repeats(text)) live.final({ kind: 'message', id: p.item.id, text: short(p.item.text, TEXT_CAP) })
|
|
101
110
|
} else if (itemAction(p.item, cwd)) {
|
|
@@ -117,6 +126,7 @@ async function run({ prompt, attachments, sessionId, cwd, persona, settings, abo
|
|
|
117
126
|
steering.ended()
|
|
118
127
|
if (t.status === 'failed') error = usage.stopped(stoppedBy(t.error ?? { message: 'failed' }, usage.limits))
|
|
119
128
|
if (t.status === 'interrupted') error = 'Stopped by the user.'
|
|
129
|
+
agents.endAll(t.status === 'completed' ? 'done' : 'stopped')
|
|
120
130
|
finished({ durationMs: t.durationMs ?? undefined })
|
|
121
131
|
}
|
|
122
132
|
}
|
package/src/agents/protocol.mjs
CHANGED
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
// action { kind, id, type, tool, label, target, note?, detail? }
|
|
11
11
|
// type: read | search | edit | write | run | fetch | delegate | plan | other
|
|
12
12
|
// detail: { edit: {old,new,all} | edits: [...] | content | command }
|
|
13
|
+
// agent: the id of the agent (below) whose step it is, when an agent the turn started took it
|
|
13
14
|
// action_result { kind, id, ok, preview? }
|
|
15
|
+
// agent { kind, id, type, description, status, name?, isolated?, background?, summary?,
|
|
16
|
+
// worktree?: { path, branch? }, stats?: { tools?, ms?, adds?, dels? }, agent? }
|
|
17
|
+
// an agent the turn started (Claude Code's Agent tool, Codex's spawnAgent), sent again whole
|
|
18
|
+
// each time it changes; status: running | done | failed | stopped; worktree: its own, when it
|
|
19
|
+
// has one (subagents.mjs, codex-subagents.mjs)
|
|
14
20
|
// request { kind, request: AgentRequest } needs a person; the job waits
|
|
15
21
|
// request_resolved { kind, id, answer, by } by: voice | lens | phone | portal | timeout | policy
|
|
16
22
|
// usage { kind, context: {used,window} | null, limits: AgentLimits | null } how full the thread
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// The agents Claude Code starts inside a turn (its Agent tool, once called Task), as protocol `agent`
|
|
2
|
+
// events: what kind each is, what it was asked, how it's going, and where it works: the job's folder,
|
|
3
|
+
// or a worktree of its own (`isolation: "worktree"`, under <repo>/.claude/worktrees/<name>). Its own
|
|
4
|
+
// steps carry its id as `agent` (claude.mjs); the worktree shows up in the first path one of them
|
|
5
|
+
// touches, and the finished call names it too. Each change of an agent is the whole event again, so
|
|
6
|
+
// the last one with an id is where it stands.
|
|
7
|
+
import { display } from '../git.mjs'
|
|
8
|
+
|
|
9
|
+
/** Claude Code's tool for starting an agent, by its current name and its old one. */
|
|
10
|
+
export const isAgentCall = name => name === 'Agent' || name === 'Task'
|
|
11
|
+
|
|
12
|
+
const WORKTREE = /^(.*\/\.claude\/worktrees\/[^/]+)(?:\/|$)/
|
|
13
|
+
|
|
14
|
+
/** The agent worktree a path is inside, if any. */
|
|
15
|
+
export function worktreeOf(path) {
|
|
16
|
+
const m = WORKTREE.exec(String(path ?? ''))
|
|
17
|
+
return m ? m[1] : null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Every absolute path a tool call names, for finding where an agent works. */
|
|
21
|
+
export function pathsIn(input) {
|
|
22
|
+
const i = input ?? {}
|
|
23
|
+
const named = [i.file_path, i.notebook_path, i.path].filter(p => typeof p === 'string')
|
|
24
|
+
const cd = /(?:^|&&|;)\s*cd\s+("[^"]+"|'[^']+'|\S+)/.exec(String(i.command ?? ''))
|
|
25
|
+
if (cd) named.push(cd[1].replace(/^["']|["']$/g, ''))
|
|
26
|
+
return named.filter(p => p.startsWith('/'))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** An Agent call as the agent it starts. */
|
|
30
|
+
export function agentFrom(id, input, parent) {
|
|
31
|
+
const i = input ?? {}
|
|
32
|
+
return {
|
|
33
|
+
kind: 'agent',
|
|
34
|
+
id,
|
|
35
|
+
type: String(i.subagent_type || 'general-purpose'),
|
|
36
|
+
description: String(i.description || i.prompt || '').slice(0, 200),
|
|
37
|
+
status: 'running',
|
|
38
|
+
...(i.name ? { name: String(i.name) } : {}),
|
|
39
|
+
...(i.isolation === 'worktree' ? { isolated: true } : {}),
|
|
40
|
+
...(parent ? { agent: parent } : {}),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A finished Agent call's result (the SDK's tool_use_result): how it went, where, and what it did. */
|
|
45
|
+
export function finished(agent, result, failed) {
|
|
46
|
+
const r = result && typeof result === 'object' ? result : {}
|
|
47
|
+
if (r.status === 'async_launched') return { ...agent, background: true }
|
|
48
|
+
const s = r.toolStats
|
|
49
|
+
const stats = {
|
|
50
|
+
...(typeof r.totalToolUseCount === 'number' ? { tools: r.totalToolUseCount } : {}),
|
|
51
|
+
...(typeof r.totalDurationMs === 'number' ? { ms: r.totalDurationMs } : {}),
|
|
52
|
+
...(s ? { adds: s.linesAdded ?? 0, dels: s.linesRemoved ?? 0 } : {}),
|
|
53
|
+
}
|
|
54
|
+
// Claude Code removes an agent's worktree when it changed nothing, and then doesn't name it.
|
|
55
|
+
const removed = r.status === 'completed' && agent.isolated
|
|
56
|
+
const worktree = r.worktreePath
|
|
57
|
+
? { path: display(r.worktreePath), ...(r.worktreeBranch ? { branch: r.worktreeBranch } : {}) }
|
|
58
|
+
: removed
|
|
59
|
+
? null
|
|
60
|
+
: agent.worktree
|
|
61
|
+
const { worktree: _, ...rest } = agent
|
|
62
|
+
return {
|
|
63
|
+
...rest,
|
|
64
|
+
status: failed ? 'failed' : 'done',
|
|
65
|
+
...(worktree ? { worktree } : {}),
|
|
66
|
+
...(Object.keys(stats).length ? { stats } : {}),
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ENDED = { completed: 'done', failed: 'failed', stopped: 'stopped' }
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A turn's agents as they go. `emit` sends each change; `branchOf(path)` reads a worktree's branch
|
|
74
|
+
* (git, or a stand-in in tests).
|
|
75
|
+
*/
|
|
76
|
+
export function subagentFeed(emit, branchOf) {
|
|
77
|
+
const agents = new Map()
|
|
78
|
+
const send = (a, more = {}) => {
|
|
79
|
+
agents.set(a.id, a)
|
|
80
|
+
emit({ ...a, ...more })
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
has: id => agents.has(id),
|
|
84
|
+
/** The Agent call: it starts. */
|
|
85
|
+
called(id, input, parent) {
|
|
86
|
+
send(agentFrom(id, input, parent))
|
|
87
|
+
},
|
|
88
|
+
/** One of its own calls: the first path inside a worktree says which one it works in. */
|
|
89
|
+
touched(id, input) {
|
|
90
|
+
const a = agents.get(id)
|
|
91
|
+
if (!a || a.worktree) return
|
|
92
|
+
const path = pathsIn(input).map(worktreeOf).find(Boolean)
|
|
93
|
+
if (!path) return
|
|
94
|
+
send({ ...a, worktree: { path: display(path) } })
|
|
95
|
+
void Promise.resolve(branchOf(path)).then(branch => {
|
|
96
|
+
const now = agents.get(id)
|
|
97
|
+
if (branch && now?.worktree && !now.worktree.branch) send({ ...now, worktree: { ...now.worktree, branch } })
|
|
98
|
+
})
|
|
99
|
+
},
|
|
100
|
+
/** A one-line summary of where it's at, while it works: shown live, never stored. */
|
|
101
|
+
progress(id, summary) {
|
|
102
|
+
const a = agents.get(id)
|
|
103
|
+
if (a && summary) emit({ ...a, summary: String(summary).slice(0, 200), transient: true })
|
|
104
|
+
},
|
|
105
|
+
/** Its call's result: done, or still going in the background. */
|
|
106
|
+
returned(id, result, failed) {
|
|
107
|
+
const a = agents.get(id)
|
|
108
|
+
if (a) send(finished(a, result, failed))
|
|
109
|
+
},
|
|
110
|
+
/** A background agent ended (task_notification). */
|
|
111
|
+
ended(id, status, summary) {
|
|
112
|
+
const a = agents.get(id)
|
|
113
|
+
if (a?.status !== 'running') return
|
|
114
|
+
send({ ...a, status: ENDED[status] ?? 'done', ...(summary ? { summary: String(summary).slice(0, 200) } : {}) })
|
|
115
|
+
},
|
|
116
|
+
/** The turn ended: whatever still runs stops with it. */
|
|
117
|
+
stopAll() {
|
|
118
|
+
for (const a of agents.values()) if (a.status === 'running') send({ ...a, status: 'stopped' })
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
}
|