@namzu/sdk 13.0.0 → 14.0.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/CHANGELOG.md +208 -0
- package/dist/agents/runAgent.d.ts +9 -0
- package/dist/agents/runAgent.d.ts.map +1 -1
- package/dist/agents/runAgent.js.map +1 -1
- package/dist/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.d.ts +2 -0
- package/dist/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.d.ts.map +1 -0
- package/dist/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.js +101 -0
- package/dist/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.js.map +1 -0
- package/dist/connector/mcp/http-sse.d.ts +10 -0
- package/dist/connector/mcp/http-sse.d.ts.map +1 -1
- package/dist/connector/mcp/http-sse.js +16 -0
- package/dist/connector/mcp/http-sse.js.map +1 -1
- package/dist/connector/mcp/stdio.d.ts +12 -0
- package/dist/connector/mcp/stdio.d.ts.map +1 -1
- package/dist/connector/mcp/stdio.js +33 -1
- package/dist/connector/mcp/stdio.js.map +1 -1
- package/dist/connector/mcp/streamable-http.d.ts +2 -0
- package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
- package/dist/connector/mcp/streamable-http.js +13 -1
- package/dist/connector/mcp/streamable-http.js.map +1 -1
- package/dist/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.d.ts +2 -0
- package/dist/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.d.ts.map +1 -0
- package/dist/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.js +73 -0
- package/dist/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.js.map +1 -0
- package/dist/contracts/schemas.d.ts +15 -0
- package/dist/contracts/schemas.d.ts.map +1 -1
- package/dist/contracts/schemas.js +18 -1
- package/dist/contracts/schemas.js.map +1 -1
- package/dist/manager/agent/__tests__/an-env-reaches-the-child-it-was-set-for.test.d.ts +2 -0
- package/dist/manager/agent/__tests__/an-env-reaches-the-child-it-was-set-for.test.d.ts.map +1 -0
- package/dist/manager/agent/__tests__/an-env-reaches-the-child-it-was-set-for.test.js +159 -0
- package/dist/manager/agent/__tests__/an-env-reaches-the-child-it-was-set-for.test.js.map +1 -0
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +39 -4
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/index.d.ts +2 -0
- package/dist/manager/index.d.ts.map +1 -1
- package/dist/manager/index.js +1 -0
- package/dist/manager/index.js.map +1 -1
- package/dist/manager/project/__tests__/a-closed-workspace-takes-no-work.test.d.ts +2 -0
- package/dist/manager/project/__tests__/a-closed-workspace-takes-no-work.test.d.ts.map +1 -0
- package/dist/manager/project/__tests__/a-closed-workspace-takes-no-work.test.js +208 -0
- package/dist/manager/project/__tests__/a-closed-workspace-takes-no-work.test.js.map +1 -0
- package/dist/manager/project/lifecycle.d.ts +66 -0
- package/dist/manager/project/lifecycle.d.ts.map +1 -0
- package/dist/manager/project/lifecycle.js +125 -0
- package/dist/manager/project/lifecycle.js.map +1 -0
- package/dist/public-runtime.d.ts +2 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +7 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/session/errors.d.ts +71 -1
- package/dist/session/errors.d.ts.map +1 -1
- package/dist/session/errors.js +50 -0
- package/dist/session/errors.js.map +1 -1
- package/dist/session/handoff/broadcast.d.ts.map +1 -1
- package/dist/session/handoff/broadcast.js +4 -5
- package/dist/session/handoff/broadcast.js.map +1 -1
- package/dist/session/handoff/single.d.ts.map +1 -1
- package/dist/session/handoff/single.js +5 -4
- package/dist/session/handoff/single.js.map +1 -1
- package/dist/session/migration/__tests__/a-migrated-project-id-is-an-id.test.d.ts +2 -0
- package/dist/session/migration/__tests__/a-migrated-project-id-is-an-id.test.d.ts.map +1 -0
- package/dist/session/migration/__tests__/a-migrated-project-id-is-an-id.test.js +75 -0
- package/dist/session/migration/__tests__/a-migrated-project-id-is-an-id.test.js.map +1 -0
- package/dist/session/migration/filesystem.d.ts.map +1 -1
- package/dist/session/migration/filesystem.js +24 -0
- package/dist/session/migration/filesystem.js.map +1 -1
- package/dist/store/session/__tests__/a-workspace-can-be-configured.test.d.ts +2 -0
- package/dist/store/session/__tests__/a-workspace-can-be-configured.test.d.ts.map +1 -0
- package/dist/store/session/__tests__/a-workspace-can-be-configured.test.js +149 -0
- package/dist/store/session/__tests__/a-workspace-can-be-configured.test.js.map +1 -0
- package/dist/store/session/disk.d.ts +6 -2
- package/dist/store/session/disk.d.ts.map +1 -1
- package/dist/store/session/disk.js +120 -3
- package/dist/store/session/disk.js.map +1 -1
- package/dist/store/session/memory.d.ts +6 -2
- package/dist/store/session/memory.d.ts.map +1 -1
- package/dist/store/session/memory.js +78 -3
- package/dist/store/session/memory.js.map +1 -1
- package/dist/tools/coordinator/agent.d.ts.map +1 -1
- package/dist/tools/coordinator/agent.js +7 -0
- package/dist/tools/coordinator/agent.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +6 -0
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/types/agent/base.d.ts +22 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/project/entity.d.ts +17 -0
- package/dist/types/project/entity.d.ts.map +1 -1
- package/dist/types/session/store.d.ts +80 -1
- package/dist/types/session/store.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agents/runAgent.ts +9 -0
- package/src/connector/mcp/__tests__/a-transport-forgets-its-listeners.test.ts +127 -0
- package/src/connector/mcp/http-sse.ts +17 -0
- package/src/connector/mcp/stdio.ts +34 -1
- package/src/connector/mcp/streamable-http.ts +15 -1
- package/src/contracts/__tests__/an-id-the-sdk-mints-passes-its-own-schema.test.ts +84 -0
- package/src/contracts/schemas.ts +18 -1
- package/src/manager/agent/__tests__/an-env-reaches-the-child-it-was-set-for.test.ts +196 -0
- package/src/manager/agent/lifecycle.ts +42 -6
- package/src/manager/index.ts +3 -0
- package/src/manager/project/__tests__/a-closed-workspace-takes-no-work.test.ts +268 -0
- package/src/manager/project/lifecycle.ts +160 -0
- package/src/public-runtime.ts +9 -0
- package/src/session/errors.ts +85 -1
- package/src/session/handoff/broadcast.ts +4 -5
- package/src/session/handoff/single.ts +5 -4
- package/src/session/migration/__tests__/a-migrated-project-id-is-an-id.test.ts +92 -0
- package/src/session/migration/filesystem.ts +28 -0
- package/src/store/session/__tests__/a-workspace-can-be-configured.test.ts +210 -0
- package/src/store/session/disk.ts +142 -4
- package/src/store/session/memory.ts +95 -4
- package/src/tools/coordinator/agent.ts +7 -0
- package/src/tools/coordinator/index.ts +6 -0
- package/src/types/agent/base.ts +23 -0
- package/src/types/project/entity.ts +18 -0
- package/src/types/session/store.ts +95 -1
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
|
|
24
24
|
import { appendFile, mkdir, readFile, readdir, rm } from 'node:fs/promises'
|
|
25
25
|
import { join } from 'node:path'
|
|
26
|
-
import { StaleSessionError, TenantIsolationError } from '../../session/errors.js'
|
|
26
|
+
import { StaleProjectError, StaleSessionError, TenantIsolationError } from '../../session/errors.js'
|
|
27
27
|
import { SessionAlreadySummarizedError } from '../../session/summary/errors.js'
|
|
28
28
|
import type { MessageId, SessionId, TenantId } from '../../types/ids/index.js'
|
|
29
29
|
import type { Message } from '../../types/message/index.js'
|
|
30
|
-
import type { Project } from '../../types/project/entity.js'
|
|
30
|
+
import type { Project, ProjectStatus } from '../../types/project/entity.js'
|
|
31
31
|
import type { Session } from '../../types/session/entity.js'
|
|
32
32
|
import type { ProjectId, SubSessionId, SummaryId, ThreadId } from '../../types/session/ids.js'
|
|
33
33
|
import type { SessionMessage } from '../../types/session/messages.js'
|
|
@@ -35,6 +35,7 @@ import type {
|
|
|
35
35
|
CreateProjectParams,
|
|
36
36
|
CreateSessionParams,
|
|
37
37
|
CreateSubSessionParams,
|
|
38
|
+
ProjectConfigInput,
|
|
38
39
|
SessionStore,
|
|
39
40
|
SessionView,
|
|
40
41
|
} from '../../types/session/store.js'
|
|
@@ -79,6 +80,10 @@ interface PersistedProject {
|
|
|
79
80
|
tenantId: TenantId
|
|
80
81
|
name: string
|
|
81
82
|
config: Project['config']
|
|
83
|
+
/** Absent in files written before the workspace gained a status. */
|
|
84
|
+
status?: ProjectStatus
|
|
85
|
+
/** Absent in files written before the workspace gained a CAS counter. */
|
|
86
|
+
ownerVersion?: number
|
|
82
87
|
createdAt: string
|
|
83
88
|
updatedAt: string
|
|
84
89
|
}
|
|
@@ -193,10 +198,12 @@ export class DiskSessionStore implements SessionStore {
|
|
|
193
198
|
tenantId,
|
|
194
199
|
name: params.name,
|
|
195
200
|
config: {
|
|
196
|
-
maxDelegationDepth: 4,
|
|
197
|
-
maxDelegationWidth: 8,
|
|
201
|
+
maxDelegationDepth: params.config?.maxDelegationDepth ?? 4,
|
|
202
|
+
maxDelegationWidth: params.config?.maxDelegationWidth ?? 8,
|
|
198
203
|
maxInterventionDepth: 10,
|
|
199
204
|
},
|
|
205
|
+
status: 'open',
|
|
206
|
+
ownerVersion: 0,
|
|
200
207
|
createdAt: now,
|
|
201
208
|
updatedAt: now,
|
|
202
209
|
}
|
|
@@ -215,6 +222,95 @@ export class DiskSessionStore implements SessionStore {
|
|
|
215
222
|
return deserializeProject(raw)
|
|
216
223
|
}
|
|
217
224
|
|
|
225
|
+
async updateProject(
|
|
226
|
+
projectId: ProjectId,
|
|
227
|
+
config: ProjectConfigInput,
|
|
228
|
+
tenantId: TenantId,
|
|
229
|
+
): Promise<Project | null> {
|
|
230
|
+
const existing = await this.getProject(projectId, tenantId)
|
|
231
|
+
if (!existing) return null
|
|
232
|
+
// Per field, like the in-memory store: an omitted limit is left alone
|
|
233
|
+
// rather than reset.
|
|
234
|
+
const project: Project = {
|
|
235
|
+
...existing,
|
|
236
|
+
config: {
|
|
237
|
+
...existing.config,
|
|
238
|
+
...(config.maxDelegationDepth !== undefined
|
|
239
|
+
? { maxDelegationDepth: config.maxDelegationDepth }
|
|
240
|
+
: {}),
|
|
241
|
+
...(config.maxDelegationWidth !== undefined
|
|
242
|
+
? { maxDelegationWidth: config.maxDelegationWidth }
|
|
243
|
+
: {}),
|
|
244
|
+
},
|
|
245
|
+
updatedAt: new Date(),
|
|
246
|
+
}
|
|
247
|
+
await atomicWriteJson(
|
|
248
|
+
join(this.projectDir(projectId), 'project.json'),
|
|
249
|
+
serializeProject(project),
|
|
250
|
+
)
|
|
251
|
+
return project
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async setProjectStatus(
|
|
255
|
+
projectId: ProjectId,
|
|
256
|
+
status: ProjectStatus,
|
|
257
|
+
tenantId: TenantId,
|
|
258
|
+
expectedOwnerVersion: number,
|
|
259
|
+
): Promise<Project | null> {
|
|
260
|
+
const existing = await this.getProject(projectId, tenantId)
|
|
261
|
+
if (!existing) return null
|
|
262
|
+
// Against the version on disk, not the caller's copy of it.
|
|
263
|
+
if (existing.ownerVersion !== expectedOwnerVersion) {
|
|
264
|
+
throw new StaleProjectError({
|
|
265
|
+
projectId,
|
|
266
|
+
expectedOwnerVersion,
|
|
267
|
+
actualOwnerVersion: existing.ownerVersion,
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
const project: Project = {
|
|
271
|
+
...existing,
|
|
272
|
+
status,
|
|
273
|
+
ownerVersion: existing.ownerVersion + 1,
|
|
274
|
+
updatedAt: new Date(),
|
|
275
|
+
}
|
|
276
|
+
await atomicWriteJson(
|
|
277
|
+
join(this.projectDir(projectId), 'project.json'),
|
|
278
|
+
serializeProject(project),
|
|
279
|
+
)
|
|
280
|
+
return project
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async listProjects(tenantId: TenantId): Promise<readonly Project[]> {
|
|
284
|
+
// Read from the directory rather than the lazily-built index: the index
|
|
285
|
+
// only knows about projects this instance has already touched, so a
|
|
286
|
+
// listing built from it would omit everything written by a previous
|
|
287
|
+
// process — which for a store whose whole point is durability is the
|
|
288
|
+
// wrong answer.
|
|
289
|
+
const projectsRoot = join(this.rootDir, 'projects')
|
|
290
|
+
let entries: string[]
|
|
291
|
+
try {
|
|
292
|
+
entries = await readdir(projectsRoot)
|
|
293
|
+
} catch {
|
|
294
|
+
return []
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const found: Project[] = []
|
|
298
|
+
for (const entry of entries) {
|
|
299
|
+
const raw = await readJson<PersistedProject>(join(projectsRoot, entry, 'project.json'))
|
|
300
|
+
if (!raw) continue
|
|
301
|
+
// Another tenant's project is absent, not an error — a listing is a
|
|
302
|
+
// question about what you own, and refusing would leak that
|
|
303
|
+
// somebody else's project is there.
|
|
304
|
+
if (raw.tenantId !== tenantId) continue
|
|
305
|
+
found.push(deserializeProject(raw))
|
|
306
|
+
}
|
|
307
|
+
// Tie-broken by id — see the in-memory store. On a fast filesystem two
|
|
308
|
+
// projects share a millisecond routinely, and without this the order
|
|
309
|
+
// came from readdir.
|
|
310
|
+
found.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id))
|
|
311
|
+
return found
|
|
312
|
+
}
|
|
313
|
+
|
|
218
314
|
// Session CRUD ------------------------------------------------------------
|
|
219
315
|
|
|
220
316
|
async createSession(params: CreateSessionParams, tenantId: TenantId): Promise<Session> {
|
|
@@ -308,6 +404,40 @@ export class DiskSessionStore implements SessionStore {
|
|
|
308
404
|
return results
|
|
309
405
|
}
|
|
310
406
|
|
|
407
|
+
async listSessionsByProject(
|
|
408
|
+
projectId: ProjectId,
|
|
409
|
+
tenantId: TenantId,
|
|
410
|
+
): Promise<readonly Session[]> {
|
|
411
|
+
// One directory, not the whole root: sessions live under their project,
|
|
412
|
+
// so this is the cheap direction. `listSessions` has to scan every
|
|
413
|
+
// project precisely because `threadId` is denormalised onto the record
|
|
414
|
+
// rather than expressed in the layout.
|
|
415
|
+
const sessionsRoot = join(this.projectDir(projectId), 'sessions')
|
|
416
|
+
let sessionDirs: string[]
|
|
417
|
+
try {
|
|
418
|
+
sessionDirs = await readdir(sessionsRoot)
|
|
419
|
+
} catch (err) {
|
|
420
|
+
const code = (err as NodeJS.ErrnoException).code
|
|
421
|
+
if (code === 'ENOENT') return []
|
|
422
|
+
throw err
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const results: Session[] = []
|
|
426
|
+
for (const rawSessionId of sessionDirs) {
|
|
427
|
+
if (!rawSessionId.startsWith('ses_')) continue
|
|
428
|
+
const path = join(sessionsRoot, rawSessionId)
|
|
429
|
+
const raw = await readJson<PersistedSession>(join(path, 'session.json'))
|
|
430
|
+
if (!raw) continue
|
|
431
|
+
if (raw.tenantId !== tenantId) continue
|
|
432
|
+
results.push(deserializeSession(raw))
|
|
433
|
+
this.sessionIndex.set(raw.id, { sessionId: raw.id, projectId, path })
|
|
434
|
+
}
|
|
435
|
+
results.sort(
|
|
436
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id),
|
|
437
|
+
)
|
|
438
|
+
return results
|
|
439
|
+
}
|
|
440
|
+
|
|
311
441
|
async updateSession(
|
|
312
442
|
session: Session,
|
|
313
443
|
tenantId: TenantId,
|
|
@@ -849,6 +979,8 @@ function serializeProject(p: Project): PersistedProject {
|
|
|
849
979
|
tenantId: p.tenantId,
|
|
850
980
|
name: p.name,
|
|
851
981
|
config: p.config,
|
|
982
|
+
status: p.status,
|
|
983
|
+
ownerVersion: p.ownerVersion,
|
|
852
984
|
createdAt: p.createdAt.toISOString(),
|
|
853
985
|
updatedAt: p.updatedAt.toISOString(),
|
|
854
986
|
}
|
|
@@ -860,6 +992,12 @@ function deserializeProject(p: PersistedProject): Project {
|
|
|
860
992
|
tenantId: p.tenantId,
|
|
861
993
|
name: p.name,
|
|
862
994
|
config: p.config,
|
|
995
|
+
// A project.json written before these fields existed reads as an open
|
|
996
|
+
// workspace at version 0, which is what it was. Leaving `ownerVersion`
|
|
997
|
+
// undefined would be worse than a wrong default: every compare-and-set
|
|
998
|
+
// against it would fail, so an existing store could never be closed.
|
|
999
|
+
status: p.status ?? 'open',
|
|
1000
|
+
ownerVersion: p.ownerVersion ?? 0,
|
|
863
1001
|
createdAt: new Date(p.createdAt),
|
|
864
1002
|
updatedAt: new Date(p.updatedAt),
|
|
865
1003
|
}
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* (Convention #5 deny-by-default, session-hierarchy.md §12.2).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { StaleSessionError, TenantIsolationError } from '../../session/errors.js'
|
|
11
|
+
import { StaleProjectError, StaleSessionError, TenantIsolationError } from '../../session/errors.js'
|
|
12
12
|
import { SessionAlreadySummarizedError } from '../../session/summary/errors.js'
|
|
13
13
|
import type { MessageId, SessionId, TenantId } from '../../types/ids/index.js'
|
|
14
14
|
import type { Message } from '../../types/message/index.js'
|
|
15
|
-
import type { Project } from '../../types/project/entity.js'
|
|
15
|
+
import type { Project, ProjectStatus } from '../../types/project/entity.js'
|
|
16
16
|
import type { Session } from '../../types/session/entity.js'
|
|
17
17
|
import type { ProjectId, SubSessionId, ThreadId } from '../../types/session/ids.js'
|
|
18
18
|
import type { SessionMessage } from '../../types/session/messages.js'
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
CreateProjectParams,
|
|
21
21
|
CreateSessionParams,
|
|
22
22
|
CreateSubSessionParams,
|
|
23
|
+
ProjectConfigInput,
|
|
23
24
|
SessionStore,
|
|
24
25
|
SessionView,
|
|
25
26
|
} from '../../types/session/store.js'
|
|
@@ -88,10 +89,12 @@ export class InMemorySessionStore implements SessionStore {
|
|
|
88
89
|
tenantId,
|
|
89
90
|
name: params.name,
|
|
90
91
|
config: {
|
|
91
|
-
maxDelegationDepth: 4,
|
|
92
|
-
maxDelegationWidth: 8,
|
|
92
|
+
maxDelegationDepth: params.config?.maxDelegationDepth ?? 4,
|
|
93
|
+
maxDelegationWidth: params.config?.maxDelegationWidth ?? 8,
|
|
93
94
|
maxInterventionDepth: 10,
|
|
94
95
|
},
|
|
96
|
+
status: 'open',
|
|
97
|
+
ownerVersion: 0,
|
|
95
98
|
createdAt: now,
|
|
96
99
|
updatedAt: now,
|
|
97
100
|
}
|
|
@@ -106,6 +109,78 @@ export class InMemorySessionStore implements SessionStore {
|
|
|
106
109
|
return record.project
|
|
107
110
|
}
|
|
108
111
|
|
|
112
|
+
async updateProject(
|
|
113
|
+
projectId: ProjectId,
|
|
114
|
+
config: ProjectConfigInput,
|
|
115
|
+
tenantId: TenantId,
|
|
116
|
+
): Promise<Project | null> {
|
|
117
|
+
const record = this.projects.get(projectId)
|
|
118
|
+
if (!record) return null
|
|
119
|
+
this.assertTenant(record.tenantId, tenantId, `project(${projectId})`)
|
|
120
|
+
// Per field: an omitted limit is left alone rather than reset, because a
|
|
121
|
+
// caller raising the width is saying nothing about the depth.
|
|
122
|
+
const project: Project = {
|
|
123
|
+
...record.project,
|
|
124
|
+
config: {
|
|
125
|
+
...record.project.config,
|
|
126
|
+
...(config.maxDelegationDepth !== undefined
|
|
127
|
+
? { maxDelegationDepth: config.maxDelegationDepth }
|
|
128
|
+
: {}),
|
|
129
|
+
...(config.maxDelegationWidth !== undefined
|
|
130
|
+
? { maxDelegationWidth: config.maxDelegationWidth }
|
|
131
|
+
: {}),
|
|
132
|
+
},
|
|
133
|
+
updatedAt: new Date(),
|
|
134
|
+
}
|
|
135
|
+
this.projects.set(projectId, { tenantId, project })
|
|
136
|
+
return project
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async setProjectStatus(
|
|
140
|
+
projectId: ProjectId,
|
|
141
|
+
status: ProjectStatus,
|
|
142
|
+
tenantId: TenantId,
|
|
143
|
+
expectedOwnerVersion: number,
|
|
144
|
+
): Promise<Project | null> {
|
|
145
|
+
const record = this.projects.get(projectId)
|
|
146
|
+
if (!record) return null
|
|
147
|
+
this.assertTenant(record.tenantId, tenantId, `project(${projectId})`)
|
|
148
|
+
// Against the STORED version, not the caller's copy of it — comparing a
|
|
149
|
+
// value against itself is the shape the session CAS was written wrong in
|
|
150
|
+
// the first time.
|
|
151
|
+
if (record.project.ownerVersion !== expectedOwnerVersion) {
|
|
152
|
+
throw new StaleProjectError({
|
|
153
|
+
projectId,
|
|
154
|
+
expectedOwnerVersion,
|
|
155
|
+
actualOwnerVersion: record.project.ownerVersion,
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
const project: Project = {
|
|
159
|
+
...record.project,
|
|
160
|
+
status,
|
|
161
|
+
ownerVersion: record.project.ownerVersion + 1,
|
|
162
|
+
updatedAt: new Date(),
|
|
163
|
+
}
|
|
164
|
+
this.projects.set(projectId, { tenantId, project })
|
|
165
|
+
return project
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async listProjects(tenantId: TenantId): Promise<readonly Project[]> {
|
|
169
|
+
const matches: Project[] = []
|
|
170
|
+
for (const record of this.projects.values()) {
|
|
171
|
+
if (record.tenantId !== tenantId) continue
|
|
172
|
+
matches.push(record.project)
|
|
173
|
+
}
|
|
174
|
+
// Tie-broken by id, because two projects created in the same
|
|
175
|
+
// millisecond otherwise fall back to insertion or directory order and
|
|
176
|
+
// "oldest first" stops being a total order. A caller paginating a
|
|
177
|
+
// listing that reorders under it sees items move between pages.
|
|
178
|
+
matches.sort(
|
|
179
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id),
|
|
180
|
+
)
|
|
181
|
+
return matches
|
|
182
|
+
}
|
|
183
|
+
|
|
109
184
|
// Session CRUD ------------------------------------------------------------
|
|
110
185
|
|
|
111
186
|
async createSession(params: CreateSessionParams, tenantId: TenantId): Promise<Session> {
|
|
@@ -151,6 +226,22 @@ export class InMemorySessionStore implements SessionStore {
|
|
|
151
226
|
return matches
|
|
152
227
|
}
|
|
153
228
|
|
|
229
|
+
async listSessionsByProject(
|
|
230
|
+
projectId: ProjectId,
|
|
231
|
+
tenantId: TenantId,
|
|
232
|
+
): Promise<readonly Session[]> {
|
|
233
|
+
const matches: Session[] = []
|
|
234
|
+
for (const record of this.sessions.values()) {
|
|
235
|
+
if (record.tenantId !== tenantId) continue
|
|
236
|
+
if (record.session.projectId !== projectId) continue
|
|
237
|
+
matches.push(record.session)
|
|
238
|
+
}
|
|
239
|
+
matches.sort(
|
|
240
|
+
(a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id),
|
|
241
|
+
)
|
|
242
|
+
return matches
|
|
243
|
+
}
|
|
244
|
+
|
|
154
245
|
async updateSession(
|
|
155
246
|
session: Session,
|
|
156
247
|
tenantId: TenantId,
|
|
@@ -146,6 +146,13 @@ export function buildAgentTool(opts: AgentToolOptions): ToolDefinition {
|
|
|
146
146
|
// surface, and the one it exports as the canonical shape —
|
|
147
147
|
// did not.
|
|
148
148
|
...(context.parentSpan ? { parentSpan: context.parentSpan } : {}),
|
|
149
|
+
// The parent's environment, which is the whole point of setting
|
|
150
|
+
// one: a delegate that cannot see it runs against different
|
|
151
|
+
// services than the run that launched it, silently.
|
|
152
|
+
// `ToolContext.env` is the parent's own resolved map, per run.
|
|
153
|
+
...(Object.keys(context.env ?? {}).length > 0
|
|
154
|
+
? { configOverrides: { env: context.env } }
|
|
155
|
+
: {}),
|
|
149
156
|
})
|
|
150
157
|
|
|
151
158
|
onTaskLaunched?.(handle.taskId, {
|
|
@@ -484,6 +484,12 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
484
484
|
// Hang the child run off THIS tool's span, so the delegation
|
|
485
485
|
// shows up inside the turn that asked for it.
|
|
486
486
|
...(_context.parentSpan ? { parentSpan: _context.parentSpan } : {}),
|
|
487
|
+
// Same as the `Agent` tool: a delegate inherits the environment
|
|
488
|
+
// its parent was given, or it runs against different services
|
|
489
|
+
// than the run that asked for the work.
|
|
490
|
+
...(Object.keys(_context.env ?? {}).length > 0
|
|
491
|
+
? { configOverrides: { env: _context.env } }
|
|
492
|
+
: {}),
|
|
487
493
|
})
|
|
488
494
|
|
|
489
495
|
// Whose task this is. The inbox ignores completions for anything it
|
package/src/types/agent/base.ts
CHANGED
|
@@ -22,6 +22,29 @@ export interface BaseAgentConfig {
|
|
|
22
22
|
maxResponseTokens?: number
|
|
23
23
|
costLimitUsd?: number
|
|
24
24
|
permissionMode?: PermissionMode
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extra environment variables for this agent's tools and sandboxed
|
|
28
|
+
* commands, merged over whatever ambient environment the execution path
|
|
29
|
+
* supplies. Inherited by every delegated descendant.
|
|
30
|
+
*
|
|
31
|
+
* **Configuration, not credentials** — and that is a property of the
|
|
32
|
+
* CHANNEL rather than a judgement about any particular value. This map is
|
|
33
|
+
* copied into every child, is readable by any tool that can run a command,
|
|
34
|
+
* and enters a model's context and the run transcript the moment something
|
|
35
|
+
* echoes it. Nothing here is scoped, redacted, or revocable.
|
|
36
|
+
*
|
|
37
|
+
* A value that authenticates to a host belongs on the brokered credential
|
|
38
|
+
* path instead, where the process holds a placeholder and the real value is
|
|
39
|
+
* attached per-host on egress — so it is never in the environment, never in
|
|
40
|
+
* a transcript, and never inherited by a child that had no business with it.
|
|
41
|
+
*
|
|
42
|
+
* Inheritance was broken until it was not: a child built through a
|
|
43
|
+
* `configBuilder` never received this at all, because the builder is
|
|
44
|
+
* written by whoever registered the agent and cannot forward a field it was
|
|
45
|
+
* never told about. It is stamped after the builder returns now, for the
|
|
46
|
+
* same reason `parentSpan` and `resumeHandler` are.
|
|
47
|
+
*/
|
|
25
48
|
env?: Record<string, string>
|
|
26
49
|
|
|
27
50
|
/**
|
|
@@ -27,15 +27,33 @@ export interface ProjectConfig {
|
|
|
27
27
|
retentionPolicy?: RetentionPolicy
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Whether the workspace accepts new work.
|
|
32
|
+
*
|
|
33
|
+
* Owner-managed, like the status it replaces: a project does not derive
|
|
34
|
+
* `archived` from having no live sessions, because "empty right now" and
|
|
35
|
+
* "closed" are different facts and only one of them is a decision.
|
|
36
|
+
*/
|
|
37
|
+
export type ProjectStatus = 'open' | 'archived'
|
|
38
|
+
|
|
30
39
|
/**
|
|
31
40
|
* Long-lived goal scope that owns shared memory, vaults, knowledge bases,
|
|
32
41
|
* and deliverables across sessions. See session-hierarchy.md §4.2.
|
|
42
|
+
*
|
|
43
|
+
* `status` is the gate the ingress paths read: an archived project accepts no
|
|
44
|
+
* new session and no handoff. It lives here rather than on Thread because the
|
|
45
|
+
* project is the thing a tenant actually closes — a workspace with its own
|
|
46
|
+
* limits, its own environment, and its own memory — and closing it has to mean
|
|
47
|
+
* something to the code, not only to a listing.
|
|
33
48
|
*/
|
|
34
49
|
export interface Project {
|
|
35
50
|
id: ProjectId
|
|
36
51
|
tenantId: TenantId
|
|
37
52
|
name: string
|
|
38
53
|
config: ProjectConfig
|
|
54
|
+
status: ProjectStatus
|
|
55
|
+
/** CAS counter for status transitions. Mirrors `Session.ownerVersion`. */
|
|
56
|
+
ownerVersion: number
|
|
39
57
|
createdAt: Date
|
|
40
58
|
updatedAt: Date
|
|
41
59
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* API — extensions land alongside their consumers.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { Project } from '../../types/project/entity.js'
|
|
9
|
+
import type { Project, ProjectStatus } from '../../types/project/entity.js'
|
|
10
10
|
import type { ActorRef } from '../../types/session/actor.js'
|
|
11
11
|
import type { Session } from '../../types/session/entity.js'
|
|
12
12
|
import type { SessionMessage } from '../../types/session/messages.js'
|
|
@@ -65,9 +65,42 @@ export interface CreateSubSessionParams {
|
|
|
65
65
|
* is out of scope for this phase (session-hierarchy.md §11 defers the project
|
|
66
66
|
* store to a later phase).
|
|
67
67
|
*/
|
|
68
|
+
/**
|
|
69
|
+
* The part of a Project's configuration a caller may actually set.
|
|
70
|
+
*
|
|
71
|
+
* **Exactly the fields something reads.** `ProjectConfig` declares eight; five
|
|
72
|
+
* enforcement sites read two of them, and the other six have zero readers in
|
|
73
|
+
* production — `maxInterventionDepth` included, whose three apparent hits are
|
|
74
|
+
* all comments claiming a wiring that does not exist. Exposing those here
|
|
75
|
+
* would make a dead field *easier to set*, which is worse than leaving it
|
|
76
|
+
* unreachable: a host would configure a retention policy, get no error, and
|
|
77
|
+
* believe retention was on.
|
|
78
|
+
*
|
|
79
|
+
* The rule is the repo's own: name the code that reads a declaration before
|
|
80
|
+
* shipping it. When a field gains a reader it gains a line here in the same
|
|
81
|
+
* change, and not before.
|
|
82
|
+
*/
|
|
83
|
+
export interface ProjectConfigInput {
|
|
84
|
+
/** Read by the spawn path and both handoff paths. Default 4. */
|
|
85
|
+
maxDelegationDepth?: number
|
|
86
|
+
/** Read by the spawn path and broadcast handoff. Default 8. */
|
|
87
|
+
maxDelegationWidth?: number
|
|
88
|
+
}
|
|
89
|
+
|
|
68
90
|
export interface CreateProjectParams {
|
|
69
91
|
tenantId: TenantId
|
|
70
92
|
name: string
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Per-workspace limits. Omitted fields keep the defaults.
|
|
96
|
+
*
|
|
97
|
+
* Until this existed every project in existence ran at depth 4 / width 8,
|
|
98
|
+
* because the config was hardcoded identically in both stores and there was
|
|
99
|
+
* no way to write one afterwards. A tenant with several workspaces could
|
|
100
|
+
* not give them different limits, which is most of what having several
|
|
101
|
+
* workspaces is for.
|
|
102
|
+
*/
|
|
103
|
+
config?: ProjectConfigInput
|
|
71
104
|
}
|
|
72
105
|
|
|
73
106
|
/**
|
|
@@ -99,6 +132,56 @@ export interface SessionStore {
|
|
|
99
132
|
|
|
100
133
|
getProject(projectId: ProjectId, tenantId: TenantId): Promise<Project | null>
|
|
101
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Change a Project's limits after it exists. OPTIONAL.
|
|
137
|
+
*
|
|
138
|
+
* Optional because widening a store interface is invisible to callers and
|
|
139
|
+
* fatal to implementors: a host with its own `SessionStore` should not stop
|
|
140
|
+
* compiling because the SDK grew a method. Callers check for it; the two
|
|
141
|
+
* stores here implement it.
|
|
142
|
+
*
|
|
143
|
+
* Only the fields in {@link ProjectConfigInput} can move, and an omitted
|
|
144
|
+
* field is left alone rather than reset — a caller raising the width is not
|
|
145
|
+
* saying anything about the depth. Returns the updated Project, or `null`
|
|
146
|
+
* if it does not exist.
|
|
147
|
+
*/
|
|
148
|
+
updateProject?(
|
|
149
|
+
projectId: ProjectId,
|
|
150
|
+
config: ProjectConfigInput,
|
|
151
|
+
tenantId: TenantId,
|
|
152
|
+
): Promise<Project | null>
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Every Project this tenant owns, oldest first. OPTIONAL, same reasoning.
|
|
156
|
+
*
|
|
157
|
+
* The tenant is the isolation boundary, so this is scoped to it and to
|
|
158
|
+
* nothing else — there is no level above Project to filter by.
|
|
159
|
+
*/
|
|
160
|
+
listProjects?(tenantId: TenantId): Promise<readonly Project[]>
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Open or close a workspace. OPTIONAL, same reasoning as the two above.
|
|
164
|
+
*
|
|
165
|
+
* Compare-and-set on {@link Project.ownerVersion}: pass the version you
|
|
166
|
+
* read, and a concurrent writer makes this throw `StaleProjectError`
|
|
167
|
+
* instead of silently winning. On success the stored version is bumped.
|
|
168
|
+
*
|
|
169
|
+
* Both directions, because a workspace is long-lived and closing one by
|
|
170
|
+
* mistake should not be permanent — unlike the Thread status this replaces,
|
|
171
|
+
* which only ever went one way. Returns `null` if the project does not
|
|
172
|
+
* exist; writing to another tenant's project throws.
|
|
173
|
+
*
|
|
174
|
+
* This is the store-level write. The precondition that no live session is
|
|
175
|
+
* attached belongs to {@link import('../../manager/project/lifecycle.js').ProjectManager},
|
|
176
|
+
* because the store deliberately holds no view of what is running.
|
|
177
|
+
*/
|
|
178
|
+
setProjectStatus?(
|
|
179
|
+
projectId: ProjectId,
|
|
180
|
+
status: ProjectStatus,
|
|
181
|
+
tenantId: TenantId,
|
|
182
|
+
expectedOwnerVersion: number,
|
|
183
|
+
): Promise<Project | null>
|
|
184
|
+
|
|
102
185
|
// Session CRUD ------------------------------------------------------------
|
|
103
186
|
|
|
104
187
|
createSession(params: CreateSessionParams, tenantId: TenantId): Promise<Session>
|
|
@@ -153,6 +236,17 @@ export interface SessionStore {
|
|
|
153
236
|
*/
|
|
154
237
|
listSessions(threadId: ThreadId, tenantId: TenantId): Promise<readonly Session[]>
|
|
155
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Every Session attached to a workspace, oldest first. OPTIONAL.
|
|
241
|
+
*
|
|
242
|
+
* The project-scoped sibling of {@link SessionStore.listSessions}, and the
|
|
243
|
+
* one the archive precondition reads: closing a workspace has to know what
|
|
244
|
+
* is still running in it, and "what is running in this thread" was never
|
|
245
|
+
* the question — a project can hold sessions across many threads, and after
|
|
246
|
+
* the Thread level is removed it is the only grouping left.
|
|
247
|
+
*/
|
|
248
|
+
listSessionsByProject?(projectId: ProjectId, tenantId: TenantId): Promise<readonly Session[]>
|
|
249
|
+
|
|
156
250
|
/**
|
|
157
251
|
* Hard-delete a session. Idempotent — absent sessions succeed as a no-op.
|
|
158
252
|
* Rejects with `TenantIsolationError` on cross-tenant access.
|