@meistrari/agent-core 0.3.1-experimental.0 → 0.4.0-experimental.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/README.md +6 -0
- package/package.json +1 -1
- package/src/provenance.gen.ts +3 -3
- package/src/supervisor/agent-provider-factory.ts +6 -4
- package/src/supervisor/bootstrap-binder.ts +21 -5
- package/src/supervisor/persistence/supervisor-store.ts +43 -0
- package/src/supervisor/provider-factory.ts +2 -0
- package/src/supervisor/resident.ts +2 -2
- package/src/supervisor/runtime-handler.ts +31 -0
package/README.md
CHANGED
|
@@ -160,3 +160,9 @@ using it. Commit generated files with generator changes; CI checks reproducibili
|
|
|
160
160
|
`agent.change-settings` carries a full `settings: { model, reasoningEffort }` pair through the supervisor wire and the agent protocol. Claude and Codex apply it only at an idle turn boundary, without submitting input, and emit `session.settings.changed` with the command ID and `appliesFrom: next_turn`. Product catalog, provider compatibility and admission ordering remain the caller's responsibility.
|
|
161
161
|
|
|
162
162
|
The supervisor advertises `capabilities["model-change"] = "v1"` in health. It commits the settings command outcome and inbox status in one SQLite transaction. After a process restart, an interrupted settings command can be reapplied idempotently; prompt commands retain their uncertain-delivery policy and are never retried by this path. Confirmed settings override the immutable bootstrap defaults when resuming the provider. Prompt and user-input-answer commands may carry resolved `settings` so their execution does not rely solely on mutable defaults.
|
|
163
|
+
|
|
164
|
+
## Reconfiguration before the first command
|
|
165
|
+
|
|
166
|
+
A bound supervisor accepts a `session.bootstrap` whose body differs from its binding only while its journal has received no command and the bootstrap carries none. The new body replaces the persisted binding after the same admission as the first bootstrap, the agent runtime started for the previous configuration is closed, and the next one opens a fresh provider session (`fresh` on the provider factory) instead of resuming the bound one, so it reads the agent's files again. The supervisor store keeps that reset until a provider session starts from the new body, so a restart or a reconnect in between does not resume the previous one. Once a command arrived, a different body is rejected with `agent-core.bootstrap-configuration-conflict`, as before.
|
|
167
|
+
|
|
168
|
+
The supervisor advertises `capabilities["reconfigure-before-commands"] = "v1"` in health. A control plane uses it only on sandboxes that report it: an older sandbox keeps rejecting any other configuration after its first bootstrap.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meistrari/agent-core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0-experimental.0",
|
|
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",
|
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.
|
|
4
|
-
sha: "
|
|
5
|
-
buildTime: "2026-10-
|
|
3
|
+
version: "0.4.0-experimental.0",
|
|
4
|
+
sha: "91be1614dad53cf32cfbdceccd8c1209b21686e9",
|
|
5
|
+
buildTime: "2026-10-29T14:15:16.414Z",
|
|
6
6
|
} as const
|
|
@@ -86,10 +86,12 @@ export function createAgentSupervisorProviderFactory(
|
|
|
86
86
|
if (provider.id !== input.bootstrap.agent.provider)
|
|
87
87
|
throw new Error(`Provider factory returned ${provider.id} for ${input.bootstrap.agent.provider}.`)
|
|
88
88
|
|
|
89
|
-
const previousProviderSessionId =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
const previousProviderSessionId = input.fresh
|
|
90
|
+
? undefined
|
|
91
|
+
: await options.bindingStore.load({
|
|
92
|
+
sessionId: input.bootstrap.sessionId,
|
|
93
|
+
provider: provider.id,
|
|
94
|
+
})
|
|
93
95
|
const sessionInput = {
|
|
94
96
|
sessionId: input.bootstrap.sessionId,
|
|
95
97
|
cwd: input.bootstrap.agent.cwd,
|
|
@@ -111,11 +111,27 @@ export function createBootstrapBinder(input: {
|
|
|
111
111
|
}
|
|
112
112
|
const persisted = binding.store.binding()
|
|
113
113
|
if (!persisted || persisted.bodyHash !== sha256CanonicalJson(request.envelope.body)) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
// Before any command the configuration has run nothing, so a session may still
|
|
115
|
+
// replace it; once a command arrived it is the configuration of what ran.
|
|
116
|
+
if (!persisted || request.envelope.body.initialCommand || binding.store.lastReceivedCommandSeq() !== 0) {
|
|
117
|
+
throw new SupervisorConfigurationError({
|
|
118
|
+
code: 'agent-core.bootstrap-configuration-conflict',
|
|
119
|
+
message: 'Supervisor reconnect bootstrap differs from its immutable binding.',
|
|
120
|
+
publicMessage: 'The sandbox configuration cannot change after bootstrap.',
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
await input.admission?.assertAllowed(request.envelope.body)
|
|
125
|
+
}
|
|
126
|
+
catch (cause) {
|
|
127
|
+
throw new SupervisorConfigurationError({
|
|
128
|
+
code: 'agent-core.bootstrap-admission-rejected',
|
|
129
|
+
message: 'Supervisor bootstrap admission rejected the replacement configuration.',
|
|
130
|
+
publicMessage: 'The sandbox configuration is not allowed.',
|
|
131
|
+
cause,
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
binding.store.reconfigure(request.envelope.body)
|
|
119
135
|
}
|
|
120
136
|
}
|
|
121
137
|
const acceptedBinding = binding
|
|
@@ -158,6 +158,37 @@ export class SupervisorStore {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Replaces the bootstrap body of a binding that has not received a command yet: nothing has
|
|
163
|
+
* run under the previous configuration, so the next agent runtime starts from this one.
|
|
164
|
+
*/
|
|
165
|
+
reconfigure(body: SessionBootstrapBody): PersistedSupervisorBinding {
|
|
166
|
+
const transaction = this.database.transaction(() => {
|
|
167
|
+
const existing = this.bootstrapRow()
|
|
168
|
+
if (!existing)
|
|
169
|
+
throw new Error('Supervisor is not bound.')
|
|
170
|
+
const binding = bindingFromRow(existing)
|
|
171
|
+
if (binding.body.sessionId !== body.sessionId)
|
|
172
|
+
throw new Error('Supervisor is already bound to another session.')
|
|
173
|
+
if (body.initialCommand || this.lastReceivedCommandSeq() !== 0)
|
|
174
|
+
throw new Error('Supervisor configuration cannot change after a command.')
|
|
175
|
+
const bodyJson = canonicalJson(body)
|
|
176
|
+
const bodyHash = sha256CanonicalJson(body)
|
|
177
|
+
this.database.query(`
|
|
178
|
+
UPDATE bootstrap SET body_json = ?, body_hash = ?, updated_at = ? WHERE id = ?
|
|
179
|
+
`).run(bodyJson, bodyHash, new Date().toISOString(), bootstrapId)
|
|
180
|
+
this.database.query(`INSERT INTO metadata (key, value) VALUES ('provider_session_reset', ?)
|
|
181
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(bodyHash)
|
|
182
|
+
return { ...binding, body, bodyHash }
|
|
183
|
+
})
|
|
184
|
+
try {
|
|
185
|
+
return transaction()
|
|
186
|
+
}
|
|
187
|
+
catch (cause) {
|
|
188
|
+
throw new SupervisorPersistenceError({ message: 'Failed to reconfigure supervisor bootstrap.', cause })
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
161
192
|
acceptConnectionAttempt(input: {
|
|
162
193
|
connectionGeneration: number
|
|
163
194
|
runtimeConnectionAttemptId: string
|
|
@@ -217,6 +248,18 @@ export class SupervisorStore {
|
|
|
217
248
|
})()
|
|
218
249
|
}
|
|
219
250
|
|
|
251
|
+
/**
|
|
252
|
+
* A reconfiguration left the provider session bound before it on disk: the next agent runtime
|
|
253
|
+
* opens a new one, across restarts, until one is started from the replaced configuration.
|
|
254
|
+
*/
|
|
255
|
+
providerSessionResetPending(): boolean {
|
|
256
|
+
return Boolean(this.database.query('SELECT 1 FROM metadata WHERE key = \'provider_session_reset\'').get())
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
clearProviderSessionReset(bodyHash: string): void {
|
|
260
|
+
this.database.query('DELETE FROM metadata WHERE key = \'provider_session_reset\' AND value = ?').run(bodyHash)
|
|
261
|
+
}
|
|
262
|
+
|
|
220
263
|
/** Restore accepted defaults in application order; the bootstrap remains immutable. */
|
|
221
264
|
appliedSettings(): AgentSettingsChange | undefined {
|
|
222
265
|
const row = this.database.query<{ value: string }, []>('SELECT value FROM metadata WHERE key = \'applied_settings\'').get()
|
|
@@ -31,6 +31,8 @@ export interface SupervisorProviderFactory {
|
|
|
31
31
|
signal: AbortSignal
|
|
32
32
|
emit?: (body: WireEventBody) => void
|
|
33
33
|
onFatal?: (error: unknown) => void
|
|
34
|
+
/** Open a new provider session rather than resume the one bound before a reconfiguration. */
|
|
35
|
+
fresh?: boolean
|
|
34
36
|
}) => Promise<SupervisorAgentRuntime>
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -82,7 +82,7 @@ export async function createResidentSupervisor(input: {
|
|
|
82
82
|
})
|
|
83
83
|
}
|
|
84
84
|
else {
|
|
85
|
-
await handler.
|
|
85
|
+
await handler.rebind({
|
|
86
86
|
credentials: activation.envelope.credentials,
|
|
87
87
|
gitToken: activation.envelope.gitToken,
|
|
88
88
|
})
|
|
@@ -106,7 +106,7 @@ export async function createResidentSupervisor(input: {
|
|
|
106
106
|
binding: binding.status,
|
|
107
107
|
profile: input.profile,
|
|
108
108
|
agentCore: agentCoreProvenance(),
|
|
109
|
-
capabilities: { 'model-change': 'v1' },
|
|
109
|
+
capabilities: { 'model-change': 'v1', 'reconfigure-before-commands': 'v1' },
|
|
110
110
|
}),
|
|
111
111
|
getBoundSessionId: () => binding.status === 'bound' ? binding.sessionId : undefined,
|
|
112
112
|
getLocalEventMax: () => binding.status === 'bound' ? binding.store.localEventMax() : 0,
|
|
@@ -39,6 +39,7 @@ export class SupervisorRuntimeHandler implements RuntimeConnectionHandler {
|
|
|
39
39
|
private processing = Promise.resolve()
|
|
40
40
|
private eventAckFloor = 0
|
|
41
41
|
private stopped = false
|
|
42
|
+
private agentBodyHash: string | undefined
|
|
42
43
|
private readonly retainedFrames: string[] = []
|
|
43
44
|
private readonly ephemeralFrames = new EphemeralFrameBuffer()
|
|
44
45
|
private readonly heartbeatTimer: ReturnType<typeof setInterval>
|
|
@@ -164,6 +165,32 @@ export class SupervisorRuntimeHandler implements RuntimeConnectionHandler {
|
|
|
164
165
|
await this.agent?.refreshSecrets?.(input)
|
|
165
166
|
}
|
|
166
167
|
|
|
168
|
+
/**
|
|
169
|
+
* A bootstrap was accepted. When the binding took another configuration before any command,
|
|
170
|
+
* the agent started from the previous one is closed and the next starts from the new one;
|
|
171
|
+
* otherwise the agent keeps running with the bootstrap's secrets.
|
|
172
|
+
*/
|
|
173
|
+
async rebind(input: {
|
|
174
|
+
credentials?: EphemeralCredentials
|
|
175
|
+
gitToken?: SessionBootstrapGitToken
|
|
176
|
+
}): Promise<void> {
|
|
177
|
+
if (!this.agentBodyHash || this.agentBodyHash === this.dependencies.store.binding()?.bodyHash) {
|
|
178
|
+
if (this.agent && this.agentBodyHash)
|
|
179
|
+
this.dependencies.store.clearProviderSessionReset(this.agentBodyHash)
|
|
180
|
+
await this.refreshSecrets(input)
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
this.dependencies.credentials = input.credentials
|
|
184
|
+
this.dependencies.gitToken = input.gitToken
|
|
185
|
+
await this.processing.catch(() => undefined)
|
|
186
|
+
const agent = await this.createAgentTask?.catch(() => undefined) ?? this.agent
|
|
187
|
+
this.agent = undefined
|
|
188
|
+
this.createAgentTask = undefined
|
|
189
|
+
this.agentBodyHash = undefined
|
|
190
|
+
if (agent)
|
|
191
|
+
await agent.close({ signal: new AbortController().signal }).catch(error => this.dependencies.logger.warn({ err: error }, 'agent runtime close failed'))
|
|
192
|
+
}
|
|
193
|
+
|
|
167
194
|
async stop(reason: 'sigterm' | 'sigint' | 'fatal_error' = 'sigterm'): Promise<void> {
|
|
168
195
|
if (this.stopped)
|
|
169
196
|
return
|
|
@@ -206,14 +233,18 @@ export class SupervisorRuntimeHandler implements RuntimeConnectionHandler {
|
|
|
206
233
|
credentials: this.dependencies.credentials,
|
|
207
234
|
gitToken: this.dependencies.gitToken,
|
|
208
235
|
}
|
|
236
|
+
const fresh = this.dependencies.store.providerSessionResetPending()
|
|
237
|
+
this.agentBodyHash = binding.bodyHash
|
|
209
238
|
this.createAgentTask = this.dependencies.providerFactory.create({
|
|
210
239
|
bootstrap: { ...binding.body, agent: { ...binding.body.agent, ...this.dependencies.store.appliedSettings() } },
|
|
211
240
|
...initialSecrets,
|
|
241
|
+
...(fresh && { fresh }),
|
|
212
242
|
rpc: this.rpc,
|
|
213
243
|
signal: this.lifecycleController.signal,
|
|
214
244
|
emit: body => this.emit(body),
|
|
215
245
|
onFatal: error => this.dependencies.onFatal(error),
|
|
216
246
|
}).then(async (agent) => {
|
|
247
|
+
this.dependencies.store.clearProviderSessionReset(binding.bodyHash)
|
|
217
248
|
await agent.ready?.()
|
|
218
249
|
const previousEventMax = this.dependencies.store.localEventMax()
|
|
219
250
|
this.dependencies.store.recoverCommandDispatches()
|