@meistrari/agent-core 0.1.0 → 0.1.1
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/README.md +12 -0
- package/package.json +5 -5
- package/src/agents/claude/claude-event-mapper.ts +14 -2
- package/src/agents/claude/claude-run.ts +7 -3
- package/src/agents/codex/codex-event-mapper.ts +14 -1
- package/src/protocol/agent-event.ts +2 -1
- package/src/protocol/agent-usage.ts +14 -0
- package/src/provenance.gen.ts +3 -3
- package/src/supervisor/bootstrap-binder.ts +8 -0
- package/src/supervisor/websocket-server.ts +1 -0
- package/src/testing/in-memory-runtime-control-plane.ts +6 -5
- package/src/worker-runtime-client/command-pump.ts +0 -5
- package/src/worker-runtime-client/connection-attempt.ts +11 -2
- package/src/worker-runtime-client/ports.ts +3 -1
- package/src/worker-runtime-client/sandbox-connection-runtime.ts +1 -0
package/README.md
CHANGED
|
@@ -45,3 +45,15 @@ bunx agent-core-supervisor \
|
|
|
45
45
|
Use `--loopback-tcp` for local and image-build probes. Production images default to the protected Unix socket. The extension module exports `default`, `extensions`, or `createSupervisorExtensions()` matching `SupervisorExtensions`.
|
|
46
46
|
|
|
47
47
|
Publishing runs from `main` through `.github/workflows/publish.yml` (conventional commits decide the bump).
|
|
48
|
+
|
|
49
|
+
## Durable command outcomes
|
|
50
|
+
|
|
51
|
+
A supervisor `received` ACK advances the command delivery cursor only. Implementations of
|
|
52
|
+
`DurableEventSink.record` must atomically store each durable event, project
|
|
53
|
+
`supervisor.command.applied` or `supervisor.command.failed` into command state, and advance the
|
|
54
|
+
ACK floor. Duplicate events must not repeat projections. This keeps outcomes consistent when
|
|
55
|
+
the worker disconnects after persistence but before sending its event ACK.
|
|
56
|
+
|
|
57
|
+
The former `ConnectionAuthorityStore.projectAppliedCommand` callback has been removed.
|
|
58
|
+
Move that projection into the durable-event transaction; receiving a command does not prove
|
|
59
|
+
that the provider executed it. See `InMemoryRuntimeControlPlane.record` for the test fixture.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meistrari/agent-core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.1",
|
|
5
5
|
"packageManager": "bun@1.3.12",
|
|
6
6
|
"description": "Shared contracts and runtime modules for Tela coding-agent sandboxes: agent protocol, supervisor wire protocol, resident supervisor, worker runtime client, and Claude/Codex harness adapters.",
|
|
7
7
|
"license": "UNLICENSED",
|
|
@@ -89,9 +89,9 @@
|
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
91
|
"dependencies": {
|
|
92
|
-
"jose": "
|
|
92
|
+
"jose": "6.2.11",
|
|
93
93
|
"pino": "^9.7.0",
|
|
94
|
-
"ulid": "
|
|
94
|
+
"ulid": "3.0.2",
|
|
95
95
|
"zod": "^4.1.12"
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
@@ -100,9 +100,9 @@
|
|
|
100
100
|
"@anthropic-ai/sdk": "0.93.0",
|
|
101
101
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
102
102
|
"@types/bun": "1.3.12",
|
|
103
|
-
"@types/pg": "
|
|
103
|
+
"@types/pg": "8.23.1",
|
|
104
104
|
"eslint": "^9.30.0",
|
|
105
|
-
"pg": "
|
|
105
|
+
"pg": "8.23.0",
|
|
106
106
|
"typescript": "5.9.3"
|
|
107
107
|
}
|
|
108
108
|
}
|
|
@@ -191,8 +191,20 @@ export function mapClaudeMessage(message: SDKMessage, state: ClaudeEventMapperSt
|
|
|
191
191
|
// Claude Agent SDK result usage is per completed ask/turn for the stream-json path we use,
|
|
192
192
|
// not a monotonic counter across the long-lived query. Emit it directly so summing
|
|
193
193
|
// normalized usage events gives the run total; do not diff against prior results.
|
|
194
|
-
if (usage)
|
|
195
|
-
drafts.push({ type: 'usage', turnId, actor: mainActor, payload: { usage
|
|
194
|
+
if (usage) {
|
|
195
|
+
drafts.push({ type: 'usage', turnId, actor: mainActor, payload: { usage, accounting: {
|
|
196
|
+
observationId: message.uuid,
|
|
197
|
+
mode: 'increment',
|
|
198
|
+
scopeId: turnId,
|
|
199
|
+
includesChildren: true,
|
|
200
|
+
models: Object.fromEntries(Object.entries(message.modelUsage ?? {}).map(([model, counts]) => [model, {
|
|
201
|
+
inputTokens: counts.inputTokens,
|
|
202
|
+
outputTokens: counts.outputTokens,
|
|
203
|
+
cacheReadTokens: counts.cacheReadInputTokens,
|
|
204
|
+
cacheWriteTokens: counts.cacheCreationInputTokens,
|
|
205
|
+
}])),
|
|
206
|
+
} } })
|
|
207
|
+
}
|
|
196
208
|
endOpenReasoning(drafts, state, turnId)
|
|
197
209
|
const interruptAccepted = state.interrupt.status === 'accepted' && state.interrupt.turnId === turnId
|
|
198
210
|
drafts.push({ type: 'turn.ended', turnId, actor: mainActor, payload: turnEndedPayload(message, interruptAccepted) })
|
|
@@ -285,8 +285,8 @@ export class ClaudeRun implements AgentRun {
|
|
|
285
285
|
// session identity mismatch are provider protocol drift and fail the run closed. `cancelled`
|
|
286
286
|
// (a steered-over/aborted command) and `discarded` (the session ended with the command still
|
|
287
287
|
// queued) are valid observed states but NEGATIVE terminals — never a positive acceptance, so
|
|
288
|
-
// they
|
|
289
|
-
//
|
|
288
|
+
// they reject any pending waiter as delivery-unknown so command processing can advance.
|
|
289
|
+
// Valid positive frames without a pending
|
|
290
290
|
// waiter are internal lifecycle for already-settled or non-prompt inputs and are ignored.
|
|
291
291
|
private settleAcceptance(frame: ClaudeCommandLifecycleMessage): void {
|
|
292
292
|
const parsed = commandLifecycleFrameSchema.safeParse(frame)
|
|
@@ -302,8 +302,12 @@ export class ClaudeRun implements AgentRun {
|
|
|
302
302
|
details: { expected: this.metadata.providerSessionId, actual: parsed.data.session_id },
|
|
303
303
|
})
|
|
304
304
|
}
|
|
305
|
-
if (parsed.data.state === 'cancelled' || parsed.data.state === 'discarded')
|
|
305
|
+
if (parsed.data.state === 'cancelled' || parsed.data.state === 'discarded') {
|
|
306
|
+
this.acceptanceWaiters.get(parsed.data.command_uuid)?.reject(new AgentDeliveryUnknownError({
|
|
307
|
+
message: `Claude command ended as ${parsed.data.state} before acceptance was observed.`,
|
|
308
|
+
}))
|
|
306
309
|
return
|
|
310
|
+
}
|
|
307
311
|
this.acceptanceWaiters.get(parsed.data.command_uuid)?.resolve()
|
|
308
312
|
}
|
|
309
313
|
|
|
@@ -3,6 +3,7 @@ import type { AgentWorkStatus } from '../../protocol/agent-work'
|
|
|
3
3
|
import type { AgentEventDraft } from '../agent-event-stream'
|
|
4
4
|
import type { CodexJsonRpcMessage, CodexServerNotification } from './codex-json-rpc-client'
|
|
5
5
|
import type { ServerNotificationParamsByMethod } from './codex-protocol'
|
|
6
|
+
import { createHash } from 'node:crypto'
|
|
6
7
|
import { jsonObjectSchema, jsonValueSchema } from '../../protocol'
|
|
7
8
|
import { CANONICAL_TOOL } from '../../protocol/agent-tool-name'
|
|
8
9
|
import { createRuntimeWorkItemId } from '../agent-id'
|
|
@@ -96,7 +97,19 @@ export function mapCodexNotification(notification: CodexNotification, state: Cod
|
|
|
96
97
|
const usage = usageFromTokenUsage(notification.params.tokenUsage)
|
|
97
98
|
if (!usage)
|
|
98
99
|
return []
|
|
99
|
-
return emitOrBufferByThread({ draft: { type: 'usage', turnId: notification.params.turnId, payload: {
|
|
100
|
+
return emitOrBufferByThread({ draft: { type: 'usage', turnId: notification.params.turnId, payload: {
|
|
101
|
+
usage,
|
|
102
|
+
accounting: {
|
|
103
|
+
observationId: createHash('sha256').update(notification.params.threadId).update(JSON.stringify(notification.params.tokenUsage.total)).digest('hex'),
|
|
104
|
+
mode: 'cumulative',
|
|
105
|
+
scopeId: notification.params.threadId,
|
|
106
|
+
includesChildren: false,
|
|
107
|
+
usage: {
|
|
108
|
+
...usageFromCounts(notification.params.tokenUsage.total),
|
|
109
|
+
inputTokens: Math.max(0, notification.params.tokenUsage.total.inputTokens - notification.params.tokenUsage.total.cachedInputTokens),
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
} }, threadId: notification.params.threadId, state })
|
|
100
113
|
}
|
|
101
114
|
case 'turn/plan/updated':
|
|
102
115
|
if (!state.startedTurnIds.has(notification.params.turnId) || notification.params.threadId !== state.mainThreadId)
|
|
@@ -4,7 +4,7 @@ import { agentRunMetadataSchema } from './agent-metadata'
|
|
|
4
4
|
import { agentPayloadOverflowSchema } from './agent-overflow'
|
|
5
5
|
import { agentProviderIdSchema } from './agent-provider'
|
|
6
6
|
import { agentToolCallStatusSchema, agentToolResultSchema } from './agent-tool'
|
|
7
|
-
import { agentUsageSchema } from './agent-usage'
|
|
7
|
+
import { agentUsageAccountingSchema, agentUsageSchema } from './agent-usage'
|
|
8
8
|
import { agentUserInputAnswersSchema, agentUserInputQuestionSchema } from './agent-user-input'
|
|
9
9
|
import { agentWorkObservationSchema } from './agent-work'
|
|
10
10
|
|
|
@@ -265,6 +265,7 @@ export const usageAgentEventSchema = agentTurnEventBaseSchema.extend({
|
|
|
265
265
|
type: z.literal('usage'),
|
|
266
266
|
payload: z.object({
|
|
267
267
|
usage: agentUsageSchema,
|
|
268
|
+
accounting: agentUsageAccountingSchema.optional(),
|
|
268
269
|
}).strict(),
|
|
269
270
|
}).strict()
|
|
270
271
|
|
|
@@ -12,3 +12,17 @@ export const agentUsageSchema = z.object({
|
|
|
12
12
|
}).strict()
|
|
13
13
|
|
|
14
14
|
export type AgentUsage = z.infer<typeof agentUsageSchema>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Accounting semantics are separate from display counters. Consumers persist
|
|
18
|
+
* observations/checkpoints before acknowledging events, including across runs.
|
|
19
|
+
*/
|
|
20
|
+
export const agentUsageAccountingSchema = z.object({
|
|
21
|
+
observationId: z.string().min(1),
|
|
22
|
+
mode: z.enum(['increment', 'cumulative']),
|
|
23
|
+
scopeId: z.string().min(1),
|
|
24
|
+
includesChildren: z.boolean(),
|
|
25
|
+
usage: agentUsageSchema.optional(),
|
|
26
|
+
model: z.string().min(1).optional(),
|
|
27
|
+
models: z.record(z.string(), agentUsageSchema).optional(),
|
|
28
|
+
}).strict()
|
package/src/provenance.gen.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated by scripts/write-provenance.ts. Do not edit by hand.
|
|
2
2
|
export const generatedProvenance = {
|
|
3
|
-
version: "0.1.
|
|
4
|
-
sha: "
|
|
5
|
-
buildTime: "2026-09-
|
|
3
|
+
version: "0.1.1",
|
|
4
|
+
sha: "00ac4425fb84e86e0adabba1067485b94f45c100",
|
|
5
|
+
buildTime: "2026-09-08T17:04:18.818Z",
|
|
6
6
|
} as const
|
|
@@ -34,7 +34,12 @@ export function createBootstrapBinder(input: {
|
|
|
34
34
|
shutdown: (code: number) => void
|
|
35
35
|
logger: Logger
|
|
36
36
|
}): BootstrapBinder {
|
|
37
|
+
let pending = Promise.resolve()
|
|
37
38
|
return async (request) => {
|
|
39
|
+
const previous = pending
|
|
40
|
+
const released = Promise.withResolvers<void>()
|
|
41
|
+
pending = released.promise
|
|
42
|
+
await previous
|
|
38
43
|
try {
|
|
39
44
|
return await bind(request)
|
|
40
45
|
}
|
|
@@ -55,6 +60,9 @@ export function createBootstrapBinder(input: {
|
|
|
55
60
|
terminate: () => input.shutdown(TERMINAL_ERROR_CODE),
|
|
56
61
|
}
|
|
57
62
|
}
|
|
63
|
+
finally {
|
|
64
|
+
released.resolve()
|
|
65
|
+
}
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
async function bind(request: {
|
|
@@ -381,6 +381,7 @@ export class ResidentSupervisorWebSocketServer {
|
|
|
381
381
|
return
|
|
382
382
|
input.socket.data.terminalRejection = undefined
|
|
383
383
|
input.socket.close(internalErrorCloseCode, 'Supervisor rejection receipt was not observed.')
|
|
384
|
+
input.terminate()
|
|
384
385
|
}, terminalRejectionReceiptBudgetMs)
|
|
385
386
|
timeout.unref?.()
|
|
386
387
|
input.socket.data.terminalRejection = { terminate: input.terminate, timeout }
|
|
@@ -26,6 +26,7 @@ EphemeralEventSink, RpcDispatcher, BootstrapProvider, SessionFailureSink, Runtim
|
|
|
26
26
|
readonly ephemeralEventLog: Array<Parameters<EphemeralEventSink['publish']>[0]> = []
|
|
27
27
|
readonly readySnapshots: SupervisorAgentRunSnapshot[] = []
|
|
28
28
|
readonly projectedCommandSequences: number[] = []
|
|
29
|
+
readonly failedCommandSequences: number[] = []
|
|
29
30
|
readonly failureLog: Array<{ kind: string, code?: string, detail?: string }> = []
|
|
30
31
|
readonly rpcCalls: SupervisorRpcRequestEnvelope[] = []
|
|
31
32
|
onDurableRecord: ((event: DurableEventEnvelope) => void | Promise<void>) | undefined
|
|
@@ -105,7 +106,7 @@ EphemeralEventSink, RpcDispatcher, BootstrapProvider, SessionFailureSink, Runtim
|
|
|
105
106
|
return {
|
|
106
107
|
renewed,
|
|
107
108
|
...(renewed ? { leaseExpiresAt: new Date(Date.now() + 30_000) } : {}),
|
|
108
|
-
hasOutstandingCommand: this.projectedCommandSequences.length < this.commandLog.length
|
|
109
|
+
hasOutstandingCommand: this.projectedCommandSequences.length + this.failedCommandSequences.length < this.commandLog.length,
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
|
|
@@ -131,10 +132,6 @@ EphemeralEventSink, RpcDispatcher, BootstrapProvider, SessionFailureSink, Runtim
|
|
|
131
132
|
return this.claimed && input.generation === this.generation
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
async projectAppliedCommand(input: { commandSeq: number }): Promise<void> {
|
|
135
|
-
this.projectedCommandSequences.push(input.commandSeq)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
135
|
async initial(): Promise<CommandEnvelope | undefined> {
|
|
139
136
|
return this.commandLog[0]
|
|
140
137
|
}
|
|
@@ -157,6 +154,10 @@ EphemeralEventSink, RpcDispatcher, BootstrapProvider, SessionFailureSink, Runtim
|
|
|
157
154
|
return { status: 'duplicate', highWaterMark: this.ackFloor }
|
|
158
155
|
if (event.seq !== this.ackFloor + 1)
|
|
159
156
|
return { status: 'gap' }
|
|
157
|
+
if (event.body.type === 'supervisor.command.applied')
|
|
158
|
+
this.projectedCommandSequences.push(event.body.payload.commandSeq)
|
|
159
|
+
if (event.body.type === 'supervisor.command.failed')
|
|
160
|
+
this.failedCommandSequences.push(event.body.payload.commandSeq)
|
|
160
161
|
this.durableEventLog.push(event)
|
|
161
162
|
this.ackFloor = event.seq
|
|
162
163
|
await this.onDurableRecord?.(event)
|
|
@@ -98,11 +98,6 @@ export class SessionCommandPump {
|
|
|
98
98
|
return
|
|
99
99
|
}
|
|
100
100
|
this.afterSequence = ack.commandSeq
|
|
101
|
-
await this.dependencies.authorityStore.projectAppliedCommand({
|
|
102
|
-
sessionSandboxId: this.dependencies.sessionSandboxId,
|
|
103
|
-
runtimeConnectionId: this.dependencies.runtimeConnectionId,
|
|
104
|
-
commandSeq: ack.commandSeq,
|
|
105
|
-
})
|
|
106
101
|
}
|
|
107
102
|
}
|
|
108
103
|
}
|
|
@@ -31,6 +31,7 @@ export class SessionSandboxConnectionAttempt {
|
|
|
31
31
|
|
|
32
32
|
constructor(private readonly dependencies: {
|
|
33
33
|
claim: ConnectionClaim
|
|
34
|
+
establishmentDeadline: number
|
|
34
35
|
replicaId: string
|
|
35
36
|
trafficTokenEncryptionKey: string
|
|
36
37
|
signControlAuthority: ControlAuthoritySigner
|
|
@@ -96,7 +97,7 @@ export class SessionSandboxConnectionAttempt {
|
|
|
96
97
|
signal: this.controller.signal,
|
|
97
98
|
onMessage: frame => this.processor ? this.processor.receive(frame) : queuedFrames.push(frame),
|
|
98
99
|
onClose: close => closed.resolve(close),
|
|
99
|
-
}), performance.now() + this.dependencies.timings.dialTimeoutMs, this.controller.signal, () => new ConnectionClosedError({
|
|
100
|
+
}), Math.min(this.dependencies.establishmentDeadline, performance.now() + this.dependencies.timings.dialTimeoutMs), this.controller.signal, () => new ConnectionClosedError({
|
|
100
101
|
ready: false,
|
|
101
102
|
detail: 'Supervisor connection dial timed out.',
|
|
102
103
|
}))
|
|
@@ -160,7 +161,15 @@ export class SessionSandboxConnectionAttempt {
|
|
|
160
161
|
...secrets,
|
|
161
162
|
}))
|
|
162
163
|
|
|
163
|
-
const readiness = await
|
|
164
|
+
const readiness = await beforeDeadline(Promise.race([
|
|
165
|
+
processor.waitForReadiness({ signal: this.controller.signal }),
|
|
166
|
+
closed.promise.then((close) => {
|
|
167
|
+
throw new ConnectionClosedError({ ready: false, detail: `${close.code}:${close.reason}` })
|
|
168
|
+
}),
|
|
169
|
+
]), Math.min(this.dependencies.establishmentDeadline, performance.now() + 10_000), this.controller.signal, () => new ConnectionClosedError({
|
|
170
|
+
ready: false,
|
|
171
|
+
detail: 'Supervisor bootstrap timed out.',
|
|
172
|
+
})).catch(async (error: unknown) => {
|
|
164
173
|
if (error instanceof BootstrapRejectedAckError) {
|
|
165
174
|
await processor.acknowledgeBootstrapRejection(error.ack)
|
|
166
175
|
throw new DeterministicEstablishmentError(error.ack.errorCode, error.ack.detail, true)
|
|
@@ -44,7 +44,6 @@ export interface ConnectionAuthorityStore {
|
|
|
44
44
|
markConnectionReconnecting: (input: { sessionSandboxId: string, generation: number }) => Promise<void>
|
|
45
45
|
releaseConnection: (input: { sessionSandboxId: string, generation: number, reason: string }) => Promise<void>
|
|
46
46
|
markConnectionRetryExhausted: (input: { sessionSandboxId: string, generation: number, replicaId: string }) => Promise<boolean>
|
|
47
|
-
projectAppliedCommand: (input: { sessionSandboxId: string, runtimeConnectionId: string, commandSeq: number }) => Promise<void>
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
export interface CommandSource {
|
|
@@ -58,6 +57,9 @@ export type DurableEventResult
|
|
|
58
57
|
|
|
59
58
|
export interface DurableEventSink {
|
|
60
59
|
getAckFloor: (input: { sessionSandboxId: string }) => Promise<number>
|
|
60
|
+
// Atomically persist the event, advance the ACK floor, and project its command outcome.
|
|
61
|
+
// Only supervisor.command.applied marks a command applied; received ACKs never do.
|
|
62
|
+
// Replayed events must not duplicate projections, including after reconnect.
|
|
61
63
|
record: (input: SessionSandboxRef & { runtimeConnectionId: string, event: DurableEventEnvelope }) => Promise<DurableEventResult>
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -154,6 +154,7 @@ class OwnedConnection {
|
|
|
154
154
|
while (!this.controller.signal.aborted) {
|
|
155
155
|
const attempt = new SessionSandboxConnectionAttempt({
|
|
156
156
|
claim,
|
|
157
|
+
establishmentDeadline,
|
|
157
158
|
replicaId: this.dependencies.replicaId,
|
|
158
159
|
trafficTokenEncryptionKey: this.dependencies.trafficTokenEncryptionKey,
|
|
159
160
|
signControlAuthority: this.dependencies.signControlAuthority,
|