@astrale-os/cli 1.0.0-beta.1 → 1.0.0-beta.2
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 +7 -11
- package/dist/astrale.js +3737 -3523
- package/dist/public/connect-core.js +396 -392
- package/dist/public/keys/index.js +371 -378
- package/dist/public/paths/index.js +371 -378
- package/dist/types/lib/idp.d.ts +6 -0
- package/dist/types/lib/meta.d.ts +2 -2
- package/package.json +6 -4
- package/src/admin/binding.ts +168 -0
- package/src/admin/catalog/__tests__/client.test.ts +2 -2
- package/src/admin/catalog/client.ts +4 -9
- package/src/admin/instance/__tests__/client.test.ts +2 -2
- package/src/admin/instance/client.ts +6 -11
- package/src/commands/__tests__/auth-login.test.ts +54 -1
- package/src/commands/__tests__/call.test.ts +5 -1
- package/src/commands/__tests__/domain-install-operation.test.ts +23 -7
- package/src/commands/__tests__/domain-install-owned.test.ts +5 -1
- package/src/commands/__tests__/domain-list.test.ts +83 -4
- package/src/commands/__tests__/install-identity-override.test.ts +30 -19
- package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
- package/src/commands/__tests__/update.test.ts +23 -16
- package/src/commands/domain/install.ts +11 -15
- package/src/commands/domain/list.ts +17 -12
- package/src/commands/studio.ts +20 -0
- package/src/commands/update.ts +9 -5
- package/src/connection/__tests__/errors.test.ts +16 -12
- package/src/connection/__tests__/exchange.test.ts +6 -5
- package/src/connection/exchange.ts +5 -4
- package/src/lib/__tests__/domain-publication.test.ts +144 -0
- package/src/lib/__tests__/idp-session.test.ts +22 -0
- package/src/lib/__tests__/idp.test.ts +2 -0
- package/src/lib/__tests__/instance.test.ts +6 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
- package/src/lib/__tests__/update.test.ts +12 -0
- package/src/lib/domain-publication.ts +102 -0
- package/src/lib/idp.ts +5 -1
- package/src/lib/instance.ts +6 -1
- package/src/lib/login-flow.ts +13 -3
- package/src/lib/meta.ts +2 -2
- package/src/lib/update.ts +4 -4
- package/src/program/__tests__/program.test.ts +1 -1
- package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-lwhFo7vB.js} +1 -1
- package/studio/client/dist/assets/index-B-c-smo9.js +8 -0
- package/studio/client/dist/assets/index-BckHuAWk.js +81 -0
- package/studio/client/dist/assets/index-C4aNQ6dz.css +1 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +3 -1
- package/studio/server/agent/bridge/stdio.ts +8 -1
- package/studio/server/agent/conversation.test.ts +10 -5
- package/studio/server/agent/conversation.ts +56 -8
- package/studio/server/agent/harness/codex/loadout.ts +18 -21
- package/studio/server/agent/harness/gateway/config.ts +20 -18
- package/studio/server/agent/harness/selection.ts +9 -1
- package/studio/server/agent/prompts/system.ts +3 -2
- package/studio/server/agent/prompts/turn.ts +13 -4
- package/studio/server/agent/run/completion.ts +2 -2
- package/studio/server/agent/run/preparation.ts +6 -5
- package/studio/server/agent/run/transcript.test.ts +35 -1
- package/studio/server/agent/run/transcript.ts +169 -3
- package/studio/server/agent/run/usage.test.ts +24 -0
- package/studio/server/agent/run/usage.ts +26 -1
- package/studio/server/api/canvas.ts +44 -0
- package/studio/server/api/comments.ts +85 -0
- package/studio/server/api/context.ts +61 -0
- package/studio/server/api/deployment.ts +39 -0
- package/studio/server/api/documents.ts +55 -0
- package/studio/server/api/domain.ts +59 -0
- package/studio/server/api/http.ts +45 -0
- package/studio/server/api/project.ts +57 -0
- package/studio/server/api/schema.ts +12 -0
- package/studio/server/api/views.ts +57 -0
- package/studio/server/api/workspace.ts +60 -0
- package/studio/server/api.test.ts +147 -0
- package/studio/server/api.ts +20 -422
- package/studio/server/cache.test.ts +111 -0
- package/studio/server/cache.ts +163 -9
- package/studio/server/cli-consumers.test.ts +494 -0
- package/studio/server/cli.test.ts +88 -0
- package/studio/server/cli.ts +240 -0
- package/studio/server/client-package.ts +25 -1
- package/studio/server/domain.test.ts +30 -2
- package/studio/server/domain.ts +14 -2
- package/studio/server/environment/dotenv-preview.test.ts +39 -0
- package/studio/server/environment/dotenv-preview.ts +27 -0
- package/studio/server/environment/files.test.ts +53 -0
- package/studio/server/{state/env.ts → environment/files.ts} +7 -48
- package/studio/server/{state → handoff}/copy.ts +21 -6
- package/studio/server/handoff/service.test.ts +22 -0
- package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
- package/studio/server/instances/active.ts +72 -0
- package/studio/server/instances/deploy-record.test.ts +31 -0
- package/studio/server/instances/deploy-record.ts +37 -0
- package/studio/server/instances/deploy.ts +56 -0
- package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
- package/studio/server/instances/probe.ts +67 -0
- package/studio/server/instances/status.test.ts +12 -0
- package/studio/server/instances/status.ts +49 -0
- package/studio/server/introspect/anatomy/client-tree.ts +57 -0
- package/studio/server/introspect/anatomy/env-fields.ts +66 -0
- package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
- package/studio/server/introspect/anatomy/source.ts +181 -0
- package/studio/server/introspect/anatomy/views/legacy.ts +232 -0
- package/studio/server/introspect/anatomy/views/routes.ts +105 -0
- package/studio/server/introspect/anatomy/views.ts +58 -0
- package/studio/server/introspect/anatomy-extras.test.ts +91 -4
- package/studio/server/introspect/anatomy-extras.ts +7 -726
- package/studio/server/introspect/anatomy.ts +46 -19
- package/studio/server/introspect/bundle.ts +15 -6
- package/studio/server/introspect/canonical-schema.test.ts +79 -0
- package/studio/server/introspect/canonical-schema.ts +80 -18
- package/studio/server/introspect/config-preview.test.ts +32 -0
- package/studio/server/introspect/config-preview.ts +119 -0
- package/studio/server/introspect/diff.test.ts +7 -11
- package/studio/server/introspect/diff.ts +28 -37
- package/studio/server/introspect/extractor.ts +8 -6
- package/studio/server/introspect/hash.ts +5 -2
- package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
- package/studio/server/introspect/revision.test.ts +54 -0
- package/studio/server/introspect/revision.ts +49 -0
- package/studio/server/introspect/runtime.test.ts +63 -5
- package/studio/server/introspect/runtime.ts +49 -3
- package/studio/server/introspect/schema-ir-json.ts +181 -0
- package/studio/server/introspect/source-overlay/annotations.ts +14 -0
- package/studio/server/introspect/source-overlay/handlers.ts +681 -0
- package/studio/server/introspect/source-overlay/kernel-calls.ts +49 -0
- package/studio/server/introspect/source-overlay/project.ts +248 -0
- package/studio/server/introspect/source-overlay/spans.ts +360 -0
- package/studio/server/json.ts +46 -0
- package/studio/server/lifecycle.ts +5 -1
- package/studio/server/sse.test.ts +26 -0
- package/studio/server/sse.ts +4 -1
- package/studio/server/state/baseline.test.ts +212 -1
- package/studio/server/state/baseline.ts +145 -29
- package/studio/server/state/comments.test.ts +66 -1
- package/studio/server/state/comments.ts +173 -10
- package/studio/server/state/context.ts +34 -2
- package/studio/server/state/documents.ts +42 -1
- package/studio/server/state/integrations.ts +27 -1
- package/studio/server/state/layout.test.ts +34 -0
- package/studio/server/state/layout.ts +44 -17
- package/studio/server/state/settings.ts +29 -22
- package/studio/server/state/store.test.ts +122 -0
- package/studio/server/state/store.ts +66 -17
- package/studio/server/state/visibility.ts +22 -7
- package/studio/server/views/model.test.ts +57 -0
- package/studio/server/views/model.ts +66 -0
- package/studio/server/views/runtime.ts +40 -0
- package/studio/server/views/selection-repository.test.ts +31 -0
- package/studio/server/views/selection-repository.ts +60 -0
- package/studio/server/views/session.test.ts +73 -0
- package/studio/server/views/session.ts +166 -0
- package/studio/server/{state/views.test.ts → views/target.test.ts} +8 -159
- package/studio/server/views/target.ts +248 -0
- package/studio/server/watch.ts +1 -1
- package/studio/server/workspace/catalog.test.ts +17 -0
- package/studio/server/workspace/catalog.ts +43 -0
- package/studio/server/{state → workspace}/create.test.ts +1 -1
- package/studio/server/{state → workspace}/create.ts +1 -1
- package/studio/server/{state → workspace}/git.ts +1 -1
- package/studio/server/workspace/updates.ts +83 -0
- package/studio/server/workspace-state.ts +1 -1
- package/studio/shared/contracts/README.md +16 -0
- package/studio/shared/contracts/agent.ts +270 -0
- package/studio/shared/contracts/runtime.ts +50 -0
- package/studio/shared/contracts/schema.ts +462 -0
- package/studio/shared/contracts/surface.test.ts +319 -0
- package/studio/shared/contracts/workspace.ts +274 -0
- package/studio/shared/layout.test.ts +116 -0
- package/studio/shared/schema/identity.test.ts +58 -0
- package/studio/shared/schema/identity.ts +82 -0
- package/studio/shared/types.ts +11 -1099
- package/studio/tsconfig.json +1 -1
- package/viewer/dist/main.js +35 -35
- package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
- package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
- package/studio/server/state/catalog.ts +0 -117
- package/studio/server/state/instance.ts +0 -280
- package/studio/server/state/updates.ts +0 -63
- package/studio/server/state/views.ts +0 -535
|
@@ -31,7 +31,7 @@ export async function completeRun(
|
|
|
31
31
|
settings,
|
|
32
32
|
session,
|
|
33
33
|
resume,
|
|
34
|
-
|
|
34
|
+
renderFingerprint,
|
|
35
35
|
model,
|
|
36
36
|
effort,
|
|
37
37
|
harnessEnv,
|
|
@@ -141,7 +141,7 @@ export async function completeRun(
|
|
|
141
141
|
result.finalText.trim()
|
|
142
142
|
) {
|
|
143
143
|
try {
|
|
144
|
-
const merged = mergeReply(root,
|
|
144
|
+
const merged = mergeReply(root, renderFingerprint, result.finalText, {
|
|
145
145
|
skipByComment: liveByComment,
|
|
146
146
|
})
|
|
147
147
|
run.merge = merged
|
|
@@ -13,10 +13,10 @@ import type { Bridge } from '../bridge/grant'
|
|
|
13
13
|
import type { AgentHarness } from '../harness/adapter'
|
|
14
14
|
|
|
15
15
|
import { getBundle } from '../../cache'
|
|
16
|
+
import { refreshAuto } from '../../handoff/service'
|
|
16
17
|
import { readComments } from '../../state/comments'
|
|
17
18
|
import { readContext } from '../../state/context'
|
|
18
19
|
import { listDocuments } from '../../state/documents'
|
|
19
|
-
import { refreshAuto } from '../../state/handoff'
|
|
20
20
|
import { startBridge } from '../bridge/grant'
|
|
21
21
|
import { readConversation } from '../conversation'
|
|
22
22
|
import { getHarness, resolveHarnessConfiguration } from '../harness/selection'
|
|
@@ -36,7 +36,7 @@ export interface PreparedRun {
|
|
|
36
36
|
settings: StudioSettings
|
|
37
37
|
session: ReturnType<typeof readConversation>
|
|
38
38
|
resume?: string
|
|
39
|
-
|
|
39
|
+
renderFingerprint: string
|
|
40
40
|
model?: string
|
|
41
41
|
effort?: AgentEffort
|
|
42
42
|
harnessEnv: Record<string, string>
|
|
@@ -80,7 +80,7 @@ export async function prepareRun(
|
|
|
80
80
|
const bundle = await getBundle(domainId)
|
|
81
81
|
if (controller.signal.aborted) return { error: 'agent run canceled during setup' }
|
|
82
82
|
|
|
83
|
-
const
|
|
83
|
+
const renderFingerprint = bundle?.renderFingerprint ?? ''
|
|
84
84
|
const context = readContext(root)
|
|
85
85
|
const documents = listDocuments(root)
|
|
86
86
|
const configuration = await resolveHarnessConfiguration(root, harness)
|
|
@@ -96,7 +96,8 @@ export async function prepareRun(
|
|
|
96
96
|
: buildTurnPrompt({
|
|
97
97
|
origin: bundle?.overlay.origin ?? domainId,
|
|
98
98
|
root,
|
|
99
|
-
|
|
99
|
+
renderFingerprint,
|
|
100
|
+
schemaRevision: bundle?.schemaRevision,
|
|
100
101
|
awaitingThreads: awaiting,
|
|
101
102
|
userContext: context.user,
|
|
102
103
|
autoContext: context.auto.filter((item) => item.includeInHandoff),
|
|
@@ -150,7 +151,7 @@ export async function prepareRun(
|
|
|
150
151
|
settings,
|
|
151
152
|
session,
|
|
152
153
|
resume,
|
|
153
|
-
|
|
154
|
+
renderFingerprint,
|
|
154
155
|
model,
|
|
155
156
|
effort,
|
|
156
157
|
harnessEnv,
|
|
@@ -5,6 +5,7 @@ import { join } from 'node:path'
|
|
|
5
5
|
|
|
6
6
|
import type { AgentRun } from '../../../shared/types'
|
|
7
7
|
|
|
8
|
+
import { asJsonRecord } from '../../json'
|
|
8
9
|
import { readJson, writeJson } from '../../state/store'
|
|
9
10
|
import { currentRun, hydrateRun } from './live-state'
|
|
10
11
|
import { readLastRun } from './transcript'
|
|
@@ -37,7 +38,7 @@ test('reconciles a run orphaned by restart and hydrates its terminal snapshot',
|
|
|
37
38
|
sessionId: 'thread-1',
|
|
38
39
|
error: expect.stringContaining('studio restarted'),
|
|
39
40
|
})
|
|
40
|
-
expect(readJson
|
|
41
|
+
expect(readJson(root, '.cache/agent/last-run.json', asJsonRecord, {}).status).toBe('interrupted')
|
|
41
42
|
|
|
42
43
|
hydrateRun(domainId, root)
|
|
43
44
|
expect(currentRun(domainId)).toMatchObject({
|
|
@@ -45,3 +46,36 @@ test('reconciles a run orphaned by restart and hydrates its terminal snapshot',
|
|
|
45
46
|
sessionId: 'thread-1',
|
|
46
47
|
})
|
|
47
48
|
})
|
|
49
|
+
|
|
50
|
+
test('admits future run fields but rejects malformed persisted run structure', () => {
|
|
51
|
+
const root = mkdtempSync(join(tmpdir(), 'studio-agent-transcript-boundary-'))
|
|
52
|
+
roots.push(root)
|
|
53
|
+
const domainId = `domain-${crypto.randomUUID()}`
|
|
54
|
+
writeJson(root, '.cache/agent/last-run.json', {
|
|
55
|
+
id: crypto.randomUUID(),
|
|
56
|
+
domainId,
|
|
57
|
+
harness: 'codex',
|
|
58
|
+
status: 'succeeded',
|
|
59
|
+
createdAt: '2026-08-20T00:00:00.000Z',
|
|
60
|
+
summary: 'finished work',
|
|
61
|
+
targetCommentIds: [],
|
|
62
|
+
events: [],
|
|
63
|
+
futureRunField: { version: 2 },
|
|
64
|
+
})
|
|
65
|
+
expect(readLastRun(domainId, root)).toMatchObject({
|
|
66
|
+
status: 'succeeded',
|
|
67
|
+
summary: 'finished work',
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
writeJson(root, '.cache/agent/last-run.json', {
|
|
71
|
+
id: crypto.randomUUID(),
|
|
72
|
+
domainId,
|
|
73
|
+
harness: 'codex',
|
|
74
|
+
status: 42,
|
|
75
|
+
createdAt: '2026-08-20T00:00:00.000Z',
|
|
76
|
+
summary: 'corrupt',
|
|
77
|
+
targetCommentIds: [],
|
|
78
|
+
events: [],
|
|
79
|
+
})
|
|
80
|
+
expect(readLastRun(domainId, root)).toBeNull()
|
|
81
|
+
})
|
|
@@ -1,10 +1,176 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
AGENT_ACCESS_LEVELS,
|
|
3
|
+
AGENT_EFFORT_LEVELS,
|
|
4
|
+
type AgentEvent,
|
|
5
|
+
type AgentPromptSnapshot,
|
|
6
|
+
type AgentRun,
|
|
7
|
+
type MergeResult,
|
|
8
|
+
} from '../../../shared/types'
|
|
9
|
+
import { asBoolean, asFiniteNumber, asJsonRecord, asString, asStringArray } from '../../json'
|
|
3
10
|
import { readJson, writeJson } from '../../state/store'
|
|
4
11
|
|
|
5
12
|
const LAST_RUN_FILE = '.cache/agent/last-run.json'
|
|
6
13
|
const runFile = (id: string) => `.cache/agent/runs/${id}.json`
|
|
7
14
|
|
|
15
|
+
const RUN_STATUSES = new Set<AgentRun['status']>([
|
|
16
|
+
'queued',
|
|
17
|
+
'running',
|
|
18
|
+
'succeeded',
|
|
19
|
+
'failed',
|
|
20
|
+
'canceled',
|
|
21
|
+
'interrupted',
|
|
22
|
+
])
|
|
23
|
+
const EVENT_KINDS = new Set<AgentEvent['kind']>([
|
|
24
|
+
'status',
|
|
25
|
+
'thinking',
|
|
26
|
+
'message',
|
|
27
|
+
'tool',
|
|
28
|
+
'reply',
|
|
29
|
+
'error',
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
function decodeAgentEvent(value: unknown): AgentEvent | undefined {
|
|
33
|
+
const record = asJsonRecord(value)
|
|
34
|
+
const id = asString(record?.id)
|
|
35
|
+
const ts = asString(record?.ts)
|
|
36
|
+
const kind = asString(record?.kind)
|
|
37
|
+
const text = asString(record?.text)
|
|
38
|
+
if (!id || !ts || !kind || !EVENT_KINDS.has(kind as AgentEvent['kind']) || text === undefined)
|
|
39
|
+
return undefined
|
|
40
|
+
const tool = asString(record?.tool)
|
|
41
|
+
const target = asString(record?.target)
|
|
42
|
+
const commentId = asString(record?.commentId)
|
|
43
|
+
return {
|
|
44
|
+
id,
|
|
45
|
+
ts,
|
|
46
|
+
kind: kind as AgentEvent['kind'],
|
|
47
|
+
text,
|
|
48
|
+
...(tool === undefined ? {} : { tool }),
|
|
49
|
+
...(target === undefined ? {} : { target }),
|
|
50
|
+
...(commentId === undefined ? {} : { commentId }),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function decodeMergeResult(value: unknown): MergeResult | undefined {
|
|
55
|
+
const record = asJsonRecord(value)
|
|
56
|
+
const merged = asFiniteNumber(record?.merged)
|
|
57
|
+
const closed = asFiniteNumber(record?.closed)
|
|
58
|
+
const unknownIds = asStringArray(record?.unknownIds)
|
|
59
|
+
const schemaMismatch = asBoolean(record?.schemaMismatch)
|
|
60
|
+
if (
|
|
61
|
+
merged === undefined ||
|
|
62
|
+
!Number.isInteger(merged) ||
|
|
63
|
+
closed === undefined ||
|
|
64
|
+
!Number.isInteger(closed) ||
|
|
65
|
+
!unknownIds ||
|
|
66
|
+
schemaMismatch === undefined
|
|
67
|
+
) {
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
const pastedSchemaVersion = asString(record?.pastedSchemaVersion)
|
|
71
|
+
return {
|
|
72
|
+
merged,
|
|
73
|
+
closed,
|
|
74
|
+
unknownIds,
|
|
75
|
+
schemaMismatch,
|
|
76
|
+
...(pastedSchemaVersion === undefined ? {} : { pastedSchemaVersion }),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function decodePrompt(value: unknown): AgentPromptSnapshot | undefined {
|
|
81
|
+
const record = asJsonRecord(value)
|
|
82
|
+
const createdAt = asString(record?.createdAt)
|
|
83
|
+
const systemPrompt = asString(record?.systemPrompt)
|
|
84
|
+
const turnPrompt = asString(record?.turnPrompt)
|
|
85
|
+
const firstTurn = asBoolean(record?.firstTurn)
|
|
86
|
+
const resumed = asBoolean(record?.resumed)
|
|
87
|
+
const mcpTools = asStringArray(record?.mcpTools)
|
|
88
|
+
if (
|
|
89
|
+
!createdAt ||
|
|
90
|
+
systemPrompt === undefined ||
|
|
91
|
+
turnPrompt === undefined ||
|
|
92
|
+
firstTurn === undefined ||
|
|
93
|
+
resumed === undefined ||
|
|
94
|
+
!mcpTools
|
|
95
|
+
) {
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
const sessionId = asString(record?.sessionId)
|
|
99
|
+
const model = asString(record?.model)
|
|
100
|
+
const effort = AGENT_EFFORT_LEVELS.find((candidate) => candidate === record?.effort)
|
|
101
|
+
const access = AGENT_ACCESS_LEVELS.find((candidate) => candidate === record?.access)
|
|
102
|
+
return {
|
|
103
|
+
createdAt,
|
|
104
|
+
systemPrompt,
|
|
105
|
+
turnPrompt,
|
|
106
|
+
firstTurn,
|
|
107
|
+
resumed,
|
|
108
|
+
mcpTools,
|
|
109
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
110
|
+
...(model === undefined ? {} : { model }),
|
|
111
|
+
...(effort === undefined ? {} : { effort }),
|
|
112
|
+
...(access === undefined ? {} : { access }),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function decodeAgentRun(value: unknown): AgentRun | undefined {
|
|
117
|
+
const record = asJsonRecord(value)
|
|
118
|
+
const id = asString(record?.id)
|
|
119
|
+
const domainId = asString(record?.domainId)
|
|
120
|
+
const harness = asString(record?.harness)
|
|
121
|
+
const status = asString(record?.status)
|
|
122
|
+
const createdAt = asString(record?.createdAt)
|
|
123
|
+
const summary = asString(record?.summary)
|
|
124
|
+
const targetCommentIds = asStringArray(record?.targetCommentIds)
|
|
125
|
+
if (
|
|
126
|
+
!id ||
|
|
127
|
+
!domainId ||
|
|
128
|
+
!harness ||
|
|
129
|
+
!status ||
|
|
130
|
+
!RUN_STATUSES.has(status as AgentRun['status']) ||
|
|
131
|
+
!createdAt ||
|
|
132
|
+
summary === undefined ||
|
|
133
|
+
!targetCommentIds ||
|
|
134
|
+
!Array.isArray(record?.events)
|
|
135
|
+
) {
|
|
136
|
+
return undefined
|
|
137
|
+
}
|
|
138
|
+
const events = record.events.flatMap((event) => {
|
|
139
|
+
const decoded = decodeAgentEvent(event)
|
|
140
|
+
return decoded ? [decoded] : []
|
|
141
|
+
})
|
|
142
|
+
const finishedAt = asString(record.finishedAt)
|
|
143
|
+
const sessionId = asString(record.sessionId)
|
|
144
|
+
const resumed = asBoolean(record.resumed)
|
|
145
|
+
const costUsd = asFiniteNumber(record.costUsd)
|
|
146
|
+
const numTurns = asFiniteNumber(record.numTurns)
|
|
147
|
+
const tokens = asFiniteNumber(record.tokens)
|
|
148
|
+
const error = asString(record.error)
|
|
149
|
+
const liveReplies = asFiniteNumber(record.liveReplies)
|
|
150
|
+
const merge = decodeMergeResult(record.merge)
|
|
151
|
+
const prompt = decodePrompt(record.prompt)
|
|
152
|
+
return {
|
|
153
|
+
id,
|
|
154
|
+
domainId,
|
|
155
|
+
harness,
|
|
156
|
+
status: status as AgentRun['status'],
|
|
157
|
+
createdAt,
|
|
158
|
+
summary,
|
|
159
|
+
targetCommentIds,
|
|
160
|
+
events,
|
|
161
|
+
...(finishedAt === undefined ? {} : { finishedAt }),
|
|
162
|
+
...(sessionId === undefined ? {} : { sessionId }),
|
|
163
|
+
...(resumed === undefined ? {} : { resumed }),
|
|
164
|
+
...(costUsd === undefined ? {} : { costUsd }),
|
|
165
|
+
...(numTurns === undefined ? {} : { numTurns }),
|
|
166
|
+
...(tokens === undefined ? {} : { tokens }),
|
|
167
|
+
...(error === undefined ? {} : { error }),
|
|
168
|
+
...(liveReplies === undefined ? {} : { liveReplies }),
|
|
169
|
+
...(merge === undefined ? {} : { merge }),
|
|
170
|
+
...(prompt === undefined ? {} : { prompt }),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
8
174
|
/** Persist the latest run pointer and, when terminal, its transcript. */
|
|
9
175
|
export function persistRun(root: string, run: AgentRun, transcript = false): void {
|
|
10
176
|
try {
|
|
@@ -17,7 +183,7 @@ export function persistRun(root: string, run: AgentRun, transcript = false): voi
|
|
|
17
183
|
|
|
18
184
|
/** Rehydrate the latest run and reconcile one orphaned by a Studio restart. */
|
|
19
185
|
export function readLastRun(domainId: string, root: string): AgentRun | null {
|
|
20
|
-
const last = readJson
|
|
186
|
+
const last = readJson(root, LAST_RUN_FILE, decodeAgentRun, null)
|
|
21
187
|
if (!last || last.domainId !== domainId) return null
|
|
22
188
|
if (last.status === 'running' || last.status === 'queued') {
|
|
23
189
|
last.status = 'interrupted'
|
|
@@ -5,6 +5,7 @@ import { join } from 'node:path'
|
|
|
5
5
|
|
|
6
6
|
import type { AgentRun } from '../../../shared/types'
|
|
7
7
|
|
|
8
|
+
import { writeJson } from '../../state/store'
|
|
8
9
|
import { readUsage, recordRun } from './usage'
|
|
9
10
|
|
|
10
11
|
const roots: string[] = []
|
|
@@ -44,3 +45,26 @@ test('records only reported usage and accumulates domain totals', () => {
|
|
|
44
45
|
lastCostUsd: 0.1,
|
|
45
46
|
})
|
|
46
47
|
})
|
|
48
|
+
|
|
49
|
+
test('normalizes malformed usage fields while accepting future fields', () => {
|
|
50
|
+
const root = mkdtempSync(join(tmpdir(), 'studio-agent-usage-boundary-'))
|
|
51
|
+
roots.push(root)
|
|
52
|
+
writeJson(root, 'usage.json', {
|
|
53
|
+
runs: -2,
|
|
54
|
+
tokens: 'many',
|
|
55
|
+
costUsd: -1,
|
|
56
|
+
lastRunAt: 42,
|
|
57
|
+
lastTokens: -10,
|
|
58
|
+
futureUsageField: { version: 2 },
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(readUsage(root)).toEqual({ runs: 0, tokens: 0, costUsd: 0 })
|
|
62
|
+
|
|
63
|
+
writeJson(root, 'usage.json', {
|
|
64
|
+
runs: 3,
|
|
65
|
+
tokens: 120,
|
|
66
|
+
costUsd: 1.25,
|
|
67
|
+
futureUsageField: { version: 2 },
|
|
68
|
+
})
|
|
69
|
+
expect(readUsage(root)).toEqual({ runs: 3, tokens: 120, costUsd: 1.25 })
|
|
70
|
+
})
|
|
@@ -6,14 +6,39 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentRun, DomainUsage } from '../../../shared/types'
|
|
8
8
|
|
|
9
|
+
import { asFiniteNumber, asJsonRecord, asString } from '../../json'
|
|
9
10
|
import { readJson, writeJson } from '../../state/store'
|
|
10
11
|
|
|
11
12
|
const PATH = 'usage.json'
|
|
12
13
|
|
|
13
14
|
const EMPTY: DomainUsage = { runs: 0, tokens: 0, costUsd: 0 }
|
|
14
15
|
|
|
16
|
+
function nonNegative(value: unknown): number | undefined {
|
|
17
|
+
const number = asFiniteNumber(value)
|
|
18
|
+
return number !== undefined && number >= 0 ? number : undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function decodeUsage(value: unknown): DomainUsage | undefined {
|
|
22
|
+
const record = asJsonRecord(value)
|
|
23
|
+
if (!record) return undefined
|
|
24
|
+
const runs = nonNegative(record.runs)
|
|
25
|
+
const tokens = nonNegative(record.tokens)
|
|
26
|
+
const costUsd = nonNegative(record.costUsd)
|
|
27
|
+
const lastRunAt = asString(record.lastRunAt)
|
|
28
|
+
const lastTokens = nonNegative(record.lastTokens)
|
|
29
|
+
const lastCostUsd = nonNegative(record.lastCostUsd)
|
|
30
|
+
return {
|
|
31
|
+
runs: runs !== undefined && Number.isInteger(runs) ? runs : EMPTY.runs,
|
|
32
|
+
tokens: tokens ?? EMPTY.tokens,
|
|
33
|
+
costUsd: costUsd ?? EMPTY.costUsd,
|
|
34
|
+
...(lastRunAt === undefined ? {} : { lastRunAt }),
|
|
35
|
+
...(lastTokens === undefined ? {} : { lastTokens }),
|
|
36
|
+
...(lastCostUsd === undefined ? {} : { lastCostUsd }),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
15
40
|
export function readUsage(root: string): DomainUsage {
|
|
16
|
-
return
|
|
41
|
+
return readJson(root, PATH, decodeUsage, { ...EMPTY })
|
|
17
42
|
}
|
|
18
43
|
|
|
19
44
|
/** Fold a finished run into the running total. A run with no reported usage (e.g. it
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Persisted canvas layout and visibility preference routes. */
|
|
2
|
+
import { getBundle } from '../cache'
|
|
3
|
+
import { readLayout, resetLayout, saveLayout, setNodePositions } from '../state/layout'
|
|
4
|
+
import { readVisibility, resetVisibility, saveVisibility } from '../state/visibility'
|
|
5
|
+
import { badRequest, json, type DomainRouteContext } from './http'
|
|
6
|
+
|
|
7
|
+
export async function handleCanvasRoute(context: DomainRouteContext): Promise<Response | null> {
|
|
8
|
+
const { req, rest, body, handle } = context
|
|
9
|
+
const id = handle.id
|
|
10
|
+
const root = handle.root
|
|
11
|
+
|
|
12
|
+
if (rest === '/layout') {
|
|
13
|
+
const bundle = await getBundle(id)
|
|
14
|
+
if (req.method === 'GET') return json(readLayout(root))
|
|
15
|
+
if (body.action === 'set') {
|
|
16
|
+
return json(setNodePositions(root, body.positions ?? {}, bundle?.renderFingerprint))
|
|
17
|
+
}
|
|
18
|
+
if (body.action === 'save') {
|
|
19
|
+
return json(saveLayout(root, body.positions ?? {}, bundle?.renderFingerprint))
|
|
20
|
+
}
|
|
21
|
+
if (body.action === 'reset') {
|
|
22
|
+
resetLayout(root)
|
|
23
|
+
return json({ ok: true })
|
|
24
|
+
}
|
|
25
|
+
return badRequest('unknown layout action')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (rest === '/visibility') {
|
|
29
|
+
if (req.method === 'GET') return json(readVisibility(root))
|
|
30
|
+
if (body.action === 'set') {
|
|
31
|
+
return json(
|
|
32
|
+
saveVisibility(root, {
|
|
33
|
+
hidden: body.hidden ?? {},
|
|
34
|
+
showInheritedEdges: body.showInheritedEdges ?? true,
|
|
35
|
+
materializedInterfaces: body.materializedInterfaces ?? {},
|
|
36
|
+
}),
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
if (body.action === 'reset') return json(resetVisibility(root))
|
|
40
|
+
return badRequest('unknown visibility action')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Schema-anchored review comment routes and agent reply merge. */
|
|
2
|
+
import { getBundle } from '../cache'
|
|
3
|
+
import { schemaRefs } from '../introspect/schema-refs'
|
|
4
|
+
import {
|
|
5
|
+
addThreadEntry,
|
|
6
|
+
deleteComment,
|
|
7
|
+
editThreadEntry,
|
|
8
|
+
markOrphans,
|
|
9
|
+
mergeReply,
|
|
10
|
+
readComments,
|
|
11
|
+
setStatus,
|
|
12
|
+
upsertComment,
|
|
13
|
+
} from '../state/comments'
|
|
14
|
+
import { badRequest, json, notFound, type DomainRouteContext } from './http'
|
|
15
|
+
|
|
16
|
+
export async function handleCommentRoute(context: DomainRouteContext): Promise<Response | null> {
|
|
17
|
+
const { req, rest, body, handle, notify } = context
|
|
18
|
+
const id = handle.id
|
|
19
|
+
const root = handle.root
|
|
20
|
+
|
|
21
|
+
if (rest === '/comments') {
|
|
22
|
+
if (req.method === 'GET') {
|
|
23
|
+
const bundle = await getBundle(id)
|
|
24
|
+
const valid = new Set<string>(bundle ? schemaRefs(bundle) : [])
|
|
25
|
+
for (const key of Object.keys(bundle?.overlay.sourceSpans ?? {})) valid.add(key)
|
|
26
|
+
const store = readComments(root)
|
|
27
|
+
for (const comment of store.comments) {
|
|
28
|
+
for (const anchor of comment.anchorRefs) {
|
|
29
|
+
if (anchor.kind !== 'schema') valid.add(anchor.ref)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return json(markOrphans(root, [...valid]))
|
|
33
|
+
}
|
|
34
|
+
if (body.action === 'create') {
|
|
35
|
+
const bundle = await getBundle(id)
|
|
36
|
+
const comment = upsertComment(root, {
|
|
37
|
+
anchors: body.anchors ?? [],
|
|
38
|
+
anchorRefs: body.anchorRefs ?? [],
|
|
39
|
+
text: body.text,
|
|
40
|
+
firstRole: body.firstRole,
|
|
41
|
+
type: body.type,
|
|
42
|
+
options: body.options,
|
|
43
|
+
schemaVersion: bundle?.renderFingerprint,
|
|
44
|
+
})
|
|
45
|
+
notify({ type: 'comments', domainId: id })
|
|
46
|
+
return json(comment)
|
|
47
|
+
}
|
|
48
|
+
if (body.action === 'reply') {
|
|
49
|
+
const comment = addThreadEntry(root, body.id, body.entry)
|
|
50
|
+
notify({ type: 'comments', domainId: id })
|
|
51
|
+
return comment ? json(comment) : notFound()
|
|
52
|
+
}
|
|
53
|
+
if (body.action === 'edit') {
|
|
54
|
+
const comment = editThreadEntry(root, body.id, body.entryId, String(body.text ?? ''))
|
|
55
|
+
notify({ type: 'comments', domainId: id })
|
|
56
|
+
return comment ? json(comment) : notFound()
|
|
57
|
+
}
|
|
58
|
+
if (body.action === 'status') {
|
|
59
|
+
const comment = setStatus(root, body.id, body.status, body.closeNote)
|
|
60
|
+
notify({ type: 'comments', domainId: id })
|
|
61
|
+
return comment ? json(comment) : notFound()
|
|
62
|
+
}
|
|
63
|
+
if (body.action === 'delete') {
|
|
64
|
+
const ok = deleteComment(root, body.id)
|
|
65
|
+
notify({ type: 'comments', domainId: id })
|
|
66
|
+
return json({ ok })
|
|
67
|
+
}
|
|
68
|
+
return badRequest('unknown comments action')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (rest === '/comments/merge' && req.method === 'POST') {
|
|
72
|
+
const bundle = await getBundle(id)
|
|
73
|
+
try {
|
|
74
|
+
const result = mergeReply(root, bundle?.renderFingerprint ?? '', String(body.text ?? ''), {
|
|
75
|
+
dedupeAuthorText: true,
|
|
76
|
+
})
|
|
77
|
+
notify({ type: 'comments', domainId: id })
|
|
78
|
+
return json(result)
|
|
79
|
+
} catch (error: any) {
|
|
80
|
+
return badRequest(String(error?.message ?? error))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Authored/automatic agent context and copy-handoff routes. */
|
|
2
|
+
import { getBundle } from '../cache'
|
|
3
|
+
import { buildCopyMarkdown } from '../handoff/copy'
|
|
4
|
+
import { refreshAuto } from '../handoff/service'
|
|
5
|
+
import { readComments } from '../state/comments'
|
|
6
|
+
import {
|
|
7
|
+
addUserContext,
|
|
8
|
+
deleteContext,
|
|
9
|
+
readContext,
|
|
10
|
+
setAutoInclude,
|
|
11
|
+
updateContext,
|
|
12
|
+
} from '../state/context'
|
|
13
|
+
import { listDocuments } from '../state/documents'
|
|
14
|
+
import { badRequest, json, notFound, type DomainRouteContext } from './http'
|
|
15
|
+
|
|
16
|
+
export async function handleContextRoute(context: DomainRouteContext): Promise<Response | null> {
|
|
17
|
+
const { req, rest, body, handle, notify } = context
|
|
18
|
+
const id = handle.id
|
|
19
|
+
const root = handle.root
|
|
20
|
+
|
|
21
|
+
if (rest === '/context') {
|
|
22
|
+
if (req.method === 'GET') return json(readContext(root))
|
|
23
|
+
if (body.action === 'add') {
|
|
24
|
+
const item = addUserContext(root, body)
|
|
25
|
+
notify({ type: 'comments', domainId: id })
|
|
26
|
+
return json(item)
|
|
27
|
+
}
|
|
28
|
+
if (body.action === 'update') {
|
|
29
|
+
const item = updateContext(root, body.id, body)
|
|
30
|
+
return item ? json(item) : notFound()
|
|
31
|
+
}
|
|
32
|
+
if (body.action === 'delete') return json({ ok: deleteContext(root, body.id) })
|
|
33
|
+
if (body.action === 'include') {
|
|
34
|
+
const item = setAutoInclude(root, body.id, body.include)
|
|
35
|
+
return item ? json(item) : notFound()
|
|
36
|
+
}
|
|
37
|
+
return badRequest('unknown context action')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (rest === '/copy-payload' && req.method === 'POST') {
|
|
41
|
+
await refreshAuto(handle)
|
|
42
|
+
const bundle = await getBundle(id)
|
|
43
|
+
const storedContext = readContext(root)
|
|
44
|
+
const openComments = readComments(root).comments.filter((comment) => comment.status === 'open')
|
|
45
|
+
const markdown = buildCopyMarkdown({
|
|
46
|
+
origin: bundle?.overlay.origin ?? id,
|
|
47
|
+
root,
|
|
48
|
+
renderFingerprint: bundle?.renderFingerprint ?? '',
|
|
49
|
+
schemaRevision: bundle?.schemaRevision,
|
|
50
|
+
openComments,
|
|
51
|
+
userContext: storedContext.user,
|
|
52
|
+
autoContext: body.includeAuto
|
|
53
|
+
? storedContext.auto.filter((item) => item.includeInHandoff)
|
|
54
|
+
: [],
|
|
55
|
+
documents: listDocuments(root),
|
|
56
|
+
})
|
|
57
|
+
return json({ markdown, openComments: openComments.length })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** CLI-owned update, installation status, and deploy routes for one domain. */
|
|
2
|
+
import { getAnatomy, getBundle } from '../cache'
|
|
3
|
+
import { runDeploy } from '../instances/deploy'
|
|
4
|
+
import { instanceStatus } from '../instances/status'
|
|
5
|
+
import { applyUpdates, getUpdates } from '../workspace/updates'
|
|
6
|
+
import { json, type DomainRouteContext } from './http'
|
|
7
|
+
|
|
8
|
+
function instanceTarget(prodTarget?: string): string | null {
|
|
9
|
+
const match = prodTarget?.match(/^instance:\s*(.+)$/)
|
|
10
|
+
return match ? match[1].trim() || null : null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function handleDeploymentRoute(context: DomainRouteContext): Promise<Response | null> {
|
|
14
|
+
const { req, rest, handle } = context
|
|
15
|
+
if (rest === '/updates' && req.method === 'GET') return json(await getUpdates(handle.root))
|
|
16
|
+
if (rest === '/updates/apply' && req.method === 'POST') {
|
|
17
|
+
return json(await applyUpdates(handle.root))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (rest === '/instance') {
|
|
21
|
+
const target = instanceTarget((await getAnatomy(handle.id))?.overview.prodTarget)
|
|
22
|
+
const bundle = await getBundle(handle.id)
|
|
23
|
+
return json(
|
|
24
|
+
await instanceStatus(
|
|
25
|
+
handle,
|
|
26
|
+
target,
|
|
27
|
+
bundle?.overlay.origin ?? null,
|
|
28
|
+
bundle?.schemaRevision ?? null,
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (rest === '/instance/deploy' && req.method === 'POST') {
|
|
34
|
+
const bundle = await getBundle(handle.id)
|
|
35
|
+
return json(await runDeploy(handle, bundle?.renderFingerprint ?? null))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Context-document HTTP transport, including multipart uploads and raw bodies. */
|
|
2
|
+
import {
|
|
3
|
+
addDocument,
|
|
4
|
+
deleteDocument,
|
|
5
|
+
listDocuments,
|
|
6
|
+
readDocument,
|
|
7
|
+
updateDocument,
|
|
8
|
+
} from '../state/documents'
|
|
9
|
+
import { json, notFound, type DomainRouteContext } from './http'
|
|
10
|
+
|
|
11
|
+
/** Routes that must run before POST bodies are parsed as JSON. */
|
|
12
|
+
export async function handleDocumentTransport(
|
|
13
|
+
req: Request,
|
|
14
|
+
rest: string,
|
|
15
|
+
root: string,
|
|
16
|
+
): Promise<Response | null> {
|
|
17
|
+
if (rest === '/context/documents' && req.method === 'GET') return json(listDocuments(root))
|
|
18
|
+
if (rest === '/context/documents' && req.method === 'POST') {
|
|
19
|
+
const form = await req.formData()
|
|
20
|
+
const added = []
|
|
21
|
+
for (const value of form.getAll('files')) {
|
|
22
|
+
if (value instanceof File) {
|
|
23
|
+
added.push(
|
|
24
|
+
addDocument(root, value.name, value.type, new Uint8Array(await value.arrayBuffer())),
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return json(added)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const raw = rest.match(/^\/context\/documents\/([^/]+)\/raw$/)
|
|
32
|
+
if (raw && req.method === 'GET') {
|
|
33
|
+
const document = readDocument(root, decodeURIComponent(raw[1]))
|
|
34
|
+
if (!document) return notFound()
|
|
35
|
+
return new Response(Bun.file(document.abs), {
|
|
36
|
+
headers: { 'content-type': document.meta.type },
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** JSON document mutations dispatched with the rest of the domain routes. */
|
|
44
|
+
export function handleDocumentMutation(context: DomainRouteContext): Response | null {
|
|
45
|
+
const { req, rest, body, handle } = context
|
|
46
|
+
const root = handle.root
|
|
47
|
+
if (rest === '/context/documents/delete' && req.method === 'POST') {
|
|
48
|
+
return json({ ok: deleteDocument(root, body.id) })
|
|
49
|
+
}
|
|
50
|
+
if (rest === '/context/documents/update' && req.method === 'POST') {
|
|
51
|
+
const meta = updateDocument(root, body.id, new TextEncoder().encode(String(body.content ?? '')))
|
|
52
|
+
return meta ? json(meta) : notFound()
|
|
53
|
+
}
|
|
54
|
+
return null
|
|
55
|
+
}
|