@meistrari/agent-core 0.2.0-experimental.1 → 0.2.0-experimental.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 +50 -0
- package/docs/consumer-capability-matrix.md +374 -0
- package/package.json +17 -10
- package/scripts/generate-codex-protocol.ts +711 -0
- package/src/agents/agent-operation.ts +8 -0
- package/src/agents/agent-provider.ts +3 -0
- package/src/agents/agent-run.ts +14 -2
- package/src/agents/agent-runtime-error.ts +28 -0
- package/src/agents/agent-session-events.ts +33 -0
- package/src/agents/agent-tool-runner.ts +12 -11
- package/src/agents/agent-tool.ts +12 -4
- package/src/agents/author-context.ts +1 -1
- package/src/agents/claude/claude-auth.ts +40 -0
- package/src/agents/claude/claude-browser-turn-metadata.ts +70 -0
- package/src/agents/claude/claude-code-process.ts +98 -0
- package/src/agents/claude/claude-command-mapper.ts +63 -3
- package/src/agents/claude/claude-desktop.ts +55 -0
- package/src/agents/claude/claude-event-mapper.ts +864 -135
- package/src/agents/claude/claude-file-change.ts +128 -0
- package/src/agents/claude/claude-provider.ts +344 -29
- package/src/agents/claude/claude-run.ts +893 -130
- package/src/agents/claude/claude-selected-repository-instructions.ts +91 -0
- package/src/agents/claude/claude-skill-catalog.ts +75 -0
- package/src/agents/claude/claude-steered-notifications.ts +180 -0
- package/src/agents/claude/claude-tool-mapper.ts +81 -2
- package/src/agents/claude/claude-workspace-skills.ts +87 -0
- package/src/agents/claude/index.ts +2 -0
- package/src/agents/codex/codex-auth-mapper.ts +27 -0
- package/src/agents/codex/codex-auth.ts +19 -0
- package/src/agents/codex/codex-browser-auth-broker.ts +320 -0
- package/src/agents/codex/codex-browser-auth-status.ts +55 -0
- package/src/agents/codex/codex-command-mapper.ts +38 -3
- package/src/agents/codex/codex-event-mapper.ts +375 -64
- package/src/agents/codex/codex-json-rpc-client.ts +66 -21
- package/src/agents/codex/codex-provider.ts +118 -236
- package/src/agents/codex/codex-run-auth.ts +113 -0
- package/src/agents/codex/codex-run.ts +423 -53
- package/src/agents/codex/codex-tool-mapper.ts +2 -2
- package/src/agents/codex/codex.errors.ts +8 -0
- package/src/agents/codex/generated/meta.gen.ts +144 -6
- package/src/agents/codex/generated/namespaces.gen.ts +92 -3
- package/src/agents/codex/generated/schema.gen.ts +24698 -15638
- package/src/agents/codex/lazy-codex-run-auth.ts +47 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/input-attachment-preparation.ts +13 -0
- package/src/agents/mcp-tool-content.ts +82 -0
- package/src/agents/model-rebinding-run.ts +14 -3
- package/src/agents/selected-repository-instructions.ts +202 -0
- package/src/agents/session-context.ts +41 -0
- package/src/agents/subscription-token-refresh.ts +73 -0
- package/src/agents/user-input-request.ts +19 -20
- package/src/errors/index.ts +1 -1
- package/src/errors/validation-error.ts +6 -0
- package/src/protocol/agent-catalog.ts +159 -0
- package/src/protocol/agent-command.ts +52 -7
- package/src/protocol/agent-content.ts +36 -16
- package/src/protocol/agent-event.ts +210 -73
- package/src/protocol/agent-identity.ts +29 -0
- package/src/protocol/agent-json.ts +122 -8
- package/src/protocol/agent-metadata.ts +2 -2
- package/src/protocol/agent-model.ts +13 -1
- package/src/protocol/agent-notification.ts +110 -0
- package/src/protocol/agent-presentation.ts +71 -20
- package/src/protocol/agent-tool.ts +66 -10
- package/src/protocol/agent-usage.ts +3 -1
- package/src/protocol/agent-user-input.ts +4 -4
- package/src/protocol/agent-work.ts +12 -10
- package/src/protocol/index.ts +2 -0
- package/src/protocol/rejected-keys.ts +11 -0
- package/src/provenance.gen.ts +3 -3
- package/src/supervisor-protocol/agent-event-wrapper.ts +30 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CodexAuthProvider, CodexAuthRefreshInput, CodexAuthTokenRequest, CodexAuthTokens } from './codex-auth'
|
|
2
|
+
import type { CodexBrowserAuthSource } from './codex-browser-auth-broker'
|
|
3
|
+
import { CodexRunAuth, CodexRunAuthClosedError } from './codex-run-auth'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A `CodexRunAuth` for a run whose own provider is not Codex (Claude Browser Use). The account is
|
|
7
|
+
* bound only when the first browser connection asks for credentials, so sessions that never use
|
|
8
|
+
* Browser Use never call `codex.auth.start`. A failed binding is discarded rather than cached, so a
|
|
9
|
+
* later browser call can bind again once an account is available. Refresh semantics are exactly
|
|
10
|
+
* those of `CodexRunAuth`.
|
|
11
|
+
*/
|
|
12
|
+
export class LazyCodexRunAuth implements CodexBrowserAuthSource {
|
|
13
|
+
private current: CodexRunAuth | undefined
|
|
14
|
+
private closed = false
|
|
15
|
+
|
|
16
|
+
constructor(private readonly provider: CodexAuthProvider) {}
|
|
17
|
+
|
|
18
|
+
async getCurrentTokens(input: CodexAuthTokenRequest): Promise<CodexAuthTokens> {
|
|
19
|
+
if (this.closed)
|
|
20
|
+
throw new CodexRunAuthClosedError()
|
|
21
|
+
const auth = this.current ??= new CodexRunAuth(this.provider)
|
|
22
|
+
try {
|
|
23
|
+
await auth.acquireInitialTokens(input)
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (this.current === auth) {
|
|
27
|
+
this.current = undefined
|
|
28
|
+
auth.close()
|
|
29
|
+
}
|
|
30
|
+
throw error
|
|
31
|
+
}
|
|
32
|
+
return await auth.getCurrentTokens(input)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async refreshAuthTokens(input: CodexAuthRefreshInput): Promise<CodexAuthTokens> {
|
|
36
|
+
const auth = this.current
|
|
37
|
+
if (this.closed || !auth)
|
|
38
|
+
throw new CodexRunAuthClosedError()
|
|
39
|
+
return await auth.refreshAuthTokens(input)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
close(): void {
|
|
43
|
+
this.closed = true
|
|
44
|
+
this.current?.close()
|
|
45
|
+
this.current = undefined
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/agents/index.ts
CHANGED
|
@@ -7,8 +7,13 @@ export * from './agent-runtime-error'
|
|
|
7
7
|
export * from './agent-session-events'
|
|
8
8
|
export * from './agent-tool'
|
|
9
9
|
export * from './agent-tool-runner'
|
|
10
|
+
export * from './input-attachment.errors'
|
|
10
11
|
export * from './instructions'
|
|
11
12
|
export * from './materialized-input-attachment'
|
|
12
13
|
export * from './message-id'
|
|
14
|
+
export * from './sandbox-environment'
|
|
15
|
+
export { sessionContextText, stripSessionContext } from './session-context'
|
|
16
|
+
export type { SessionContextComposer } from './session-context'
|
|
17
|
+
export * from './subscription-token-refresh'
|
|
13
18
|
export * from './tools/ping.tool'
|
|
14
19
|
export * from './user-input-request'
|
|
@@ -56,6 +56,19 @@ export function nativeAttachmentLabel(attachment: MaterializedAgentInputAttachme
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export function filesystemFallbackLabel(attachment: MaterializedAgentInputAttachment): string {
|
|
59
|
+
if (attachment.mediaType === 'application/gzip' && attachment.filename.endsWith('.tar.gz')) {
|
|
60
|
+
return `Input attachment (gzip-compressed directory archive):\n${JSON.stringify({
|
|
61
|
+
attachmentId: attachment.attachmentId,
|
|
62
|
+
ordinal: attachment.ordinal,
|
|
63
|
+
filename: attachment.filename,
|
|
64
|
+
mediaType: attachment.mediaType,
|
|
65
|
+
byteSize: attachment.byteSize,
|
|
66
|
+
sha256: attachment.sha256,
|
|
67
|
+
path: attachment.localPath,
|
|
68
|
+
archiveFormat: 'tar+gzip',
|
|
69
|
+
usage: 'Extract or read this archive to access the attached directory files.',
|
|
70
|
+
})}`
|
|
71
|
+
}
|
|
59
72
|
return `Input attachment (filesystem fallback):\n${JSON.stringify({
|
|
60
73
|
attachmentId: attachment.attachmentId,
|
|
61
74
|
ordinal: attachment.ordinal,
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { AgentToolContent, JsonObject, JsonValue } from '../protocol'
|
|
2
|
+
import { agentToolContentSchema, jsonValueSchema } from '../protocol'
|
|
3
|
+
|
|
4
|
+
export function ownJsonSlot(value: unknown, key: string): JsonValue | undefined {
|
|
5
|
+
if (typeof value !== 'object' || value === null)
|
|
6
|
+
return undefined
|
|
7
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key)
|
|
8
|
+
if (!descriptor?.enumerable || !('value' in descriptor) || descriptor.value === undefined)
|
|
9
|
+
return undefined
|
|
10
|
+
const parsed = jsonValueSchema.safeParse(descriptor.value)
|
|
11
|
+
return parsed.success ? parsed.data : undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function mcpToolContent(value: JsonValue): AgentToolContent {
|
|
15
|
+
const content = publicMcpContent(value)
|
|
16
|
+
if (!isJsonObject(content))
|
|
17
|
+
return jsonToolContent(content)
|
|
18
|
+
const keys = content.type === 'text'
|
|
19
|
+
? ['type', 'text']
|
|
20
|
+
: content.type === 'image' || content.type === 'audio'
|
|
21
|
+
? ['type', 'data', 'mimeType']
|
|
22
|
+
: content.type === 'resource_link'
|
|
23
|
+
? ['type', 'name', 'uri', 'mimeType', 'description', 'title', 'size']
|
|
24
|
+
: content.type === 'resource'
|
|
25
|
+
? ['type', 'resource']
|
|
26
|
+
: undefined
|
|
27
|
+
if (!keys)
|
|
28
|
+
return jsonToolContent(content)
|
|
29
|
+
let candidate = withPublicExtras(content, [...keys, 'annotations'])
|
|
30
|
+
if (content.type === 'resource' && isJsonObject(content.resource)) {
|
|
31
|
+
candidate = {
|
|
32
|
+
...candidate,
|
|
33
|
+
resource: withPublicExtras(content.resource, [
|
|
34
|
+
'uri',
|
|
35
|
+
'mimeType',
|
|
36
|
+
typeof content.resource.text === 'string' ? 'text' : 'blob',
|
|
37
|
+
]),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const parsed = agentToolContentSchema.safeParse(candidate)
|
|
41
|
+
return parsed.success ? parsed.data : jsonToolContent(content)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function withPublicExtras(value: JsonObject, knownKeys: string[]): JsonObject {
|
|
45
|
+
const known = Object.entries(value).filter(([key]) => knownKeys.includes(key))
|
|
46
|
+
const extra = Object.entries(value).filter(([key]) => !knownKeys.includes(key))
|
|
47
|
+
return Object.fromEntries(extra.length > 0 ? [...known, ['extras', Object.fromEntries(extra)]] : known)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function publicMcpContent(value: JsonValue): JsonValue {
|
|
51
|
+
if (!isJsonObject(value))
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
const entries: Array<[string, JsonValue]> = []
|
|
55
|
+
for (const key of Object.keys(value)) {
|
|
56
|
+
if (key === '_meta')
|
|
57
|
+
continue
|
|
58
|
+
const entry = value[key]!
|
|
59
|
+
const publicEntry = key === 'resource' && value.type === 'resource' && isJsonObject(entry)
|
|
60
|
+
? jsonObjectWithoutKey(entry, '_meta')
|
|
61
|
+
: entry
|
|
62
|
+
entries.push([key, publicEntry])
|
|
63
|
+
}
|
|
64
|
+
return Object.fromEntries(entries)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function jsonObjectWithoutKey(value: JsonObject, omittedKey: string): JsonObject {
|
|
68
|
+
const entries: Array<[string, JsonValue]> = []
|
|
69
|
+
for (const key of Object.keys(value)) {
|
|
70
|
+
if (key !== omittedKey)
|
|
71
|
+
entries.push([key, value[key]!])
|
|
72
|
+
}
|
|
73
|
+
return Object.fromEntries(entries)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isJsonObject(value: JsonValue | undefined): value is JsonObject {
|
|
77
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function jsonToolContent(value: JsonValue): AgentToolContent {
|
|
81
|
+
return { type: 'json', value }
|
|
82
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentEvent, AgentRunMetadata, SendPromptCommand } from '../protocol'
|
|
2
|
-
import type { AgentOperationOptions } from './agent-operation'
|
|
2
|
+
import type { AgentOperationOptions, AgentPromptOperationOptions } from './agent-operation'
|
|
3
3
|
import type { AgentPromptAcceptance, AgentRun } from './agent-run'
|
|
4
4
|
import { AgentEventStream } from './agent-event-stream'
|
|
5
5
|
import { AgentNotAcceptedError, AgentPromptDeferredError, AgentRunStateError } from './agent-runtime-error'
|
|
@@ -44,8 +44,19 @@ export class ModelRebindingRun implements AgentRun {
|
|
|
44
44
|
|
|
45
45
|
respondUserInput: AgentRun['respondUserInput'] = async (command, options) => await this.withControl(signal => this.run.respondUserInput(command, { ...options, signal }), options)
|
|
46
46
|
interrupt: AgentRun['interrupt'] = async (command, options) => await this.withControl(signal => this.run.interrupt(command, { ...options, signal }), options)
|
|
47
|
+
notify: NonNullable<AgentRun['notify']> = async (command, options) => await this.withControl(async (signal) => {
|
|
48
|
+
if (!this.run.notify)
|
|
49
|
+
throw new AgentRunStateError({ message: 'The provider run does not support notifications.' })
|
|
50
|
+
return await this.run.notify(command, { ...options, signal })
|
|
51
|
+
}, options)
|
|
47
52
|
|
|
48
|
-
async
|
|
53
|
+
compactContext: NonNullable<AgentRun['compactContext']> = async (command, options) => await this.withControl(async (signal) => {
|
|
54
|
+
if (!this.run.compactContext)
|
|
55
|
+
throw new AgentRunStateError({ message: 'The provider run does not support context compaction.' })
|
|
56
|
+
return await this.run.compactContext(command, { ...options, signal })
|
|
57
|
+
}, options)
|
|
58
|
+
|
|
59
|
+
async sendPrompt(command: SendPromptCommand, options: AgentPromptOperationOptions = {}): Promise<AgentPromptAcceptance> {
|
|
49
60
|
if (this.dispatching || this.workspaceChanging || this.controls.size)
|
|
50
61
|
throw new AgentPromptDeferredError({ message: 'A prompt admission is already in progress.' })
|
|
51
62
|
const task = this.dispatchPrompt(command, options)
|
|
@@ -56,7 +67,7 @@ export class ModelRebindingRun implements AgentRun {
|
|
|
56
67
|
finally { this.dispatching = undefined }
|
|
57
68
|
}
|
|
58
69
|
|
|
59
|
-
private async dispatchPrompt(command: SendPromptCommand, options:
|
|
70
|
+
private async dispatchPrompt(command: SendPromptCommand, options: AgentPromptOperationOptions): Promise<AgentPromptAcceptance> {
|
|
60
71
|
if (this.stopped)
|
|
61
72
|
throw new AgentRunStateError({ message: 'Cannot send a prompt after the run stopped.' })
|
|
62
73
|
if (command.sessionId !== this.metadata.sessionId || command.providerSessionId !== this.metadata.providerSessionId || command.provider !== this.metadata.provider)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { AgentProviderId } from '../protocol'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import { lstat, open, readFile, rename, unlink } from 'node:fs/promises'
|
|
4
|
+
import { basename, dirname, join } from 'node:path'
|
|
5
|
+
import { AgentNotAcceptedError, AgentProviderRuntimeError } from './agent-runtime-error'
|
|
6
|
+
import { stableRepositoryRoots } from './codex/codex-skill-roots'
|
|
7
|
+
|
|
8
|
+
const managedInstructionsSentinel = '<!-- coding-agent-managed-selected-repository-instructions -->'
|
|
9
|
+
|
|
10
|
+
// Each provider reads its own repository instruction file first and falls back to AGENTS.md,
|
|
11
|
+
// matching Codex's override precedence and Claude Code's default CLAUDE.md-or-AGENTS.md mode.
|
|
12
|
+
const repositoryInstructionFiles = {
|
|
13
|
+
codex: { preferred: 'AGENTS.override.md', referencing: 'AGENTS file' },
|
|
14
|
+
claude: { preferred: 'CLAUDE.md', referencing: 'instruction file' },
|
|
15
|
+
} as const satisfies Record<AgentProviderId, { preferred: string, referencing: string }>
|
|
16
|
+
|
|
17
|
+
type ManagedFileOwnership = 'missing' | 'managed' | 'unowned'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The runtime-owned discovery guidance each adapter places in its provider's native instruction
|
|
21
|
+
* file: optional product context, the selected repository roots, and how to find each repository's
|
|
22
|
+
* own instructions. Deterministic for a given root set. Nothing is published when there is neither
|
|
23
|
+
* a repository nor product context.
|
|
24
|
+
*/
|
|
25
|
+
export function selectedRepositoryInstructions(input: { provider: AgentProviderId, repositoryRoots: readonly string[], sharedContext?: string }): string | undefined {
|
|
26
|
+
const files = repositoryInstructionFiles[input.provider]
|
|
27
|
+
const repositoryRoots = stableRepositoryRoots(input.repositoryRoots)
|
|
28
|
+
if (repositoryRoots.length === 0 && !input.sharedContext)
|
|
29
|
+
return undefined
|
|
30
|
+
const sharedContext = [
|
|
31
|
+
'# Selected repository instructions',
|
|
32
|
+
'',
|
|
33
|
+
...(input.sharedContext ? [input.sharedContext, ''] : []),
|
|
34
|
+
]
|
|
35
|
+
if (repositoryRoots.length === 0)
|
|
36
|
+
return sharedContext.join('\n')
|
|
37
|
+
|
|
38
|
+
return [
|
|
39
|
+
...sharedContext,
|
|
40
|
+
'Selected repository roots:',
|
|
41
|
+
...repositoryRoots.map(root => `- ${JSON.stringify(root)}`),
|
|
42
|
+
'',
|
|
43
|
+
'For each repository root, you must:',
|
|
44
|
+
`- Inspect \`${files.preferred}\` first; if it does not exist, inspect \`AGENTS.md\`.`,
|
|
45
|
+
`- Follow every documentation file referenced by the applicable ${files.referencing}.`,
|
|
46
|
+
`- Before editing nested files, re-check for more-local \`${files.preferred}\` or \`AGENTS.md\` files.`,
|
|
47
|
+
'',
|
|
48
|
+
'Runtime and developer instructions take precedence over these repository instructions.',
|
|
49
|
+
'Repository instructions apply only within their own repository.',
|
|
50
|
+
'If selected repositories contain irreconcilable instructions, report the conflict instead of resolving it by repository input order.',
|
|
51
|
+
'',
|
|
52
|
+
].join('\n')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Publishes the selected-repository instructions at `path`, a file the runtime owns only when its
|
|
57
|
+
* first line is the managed sentinel. An unowned regular file, symlink, or non-regular entry is
|
|
58
|
+
* never overwritten: with no selected repository it is left alone, otherwise the operation fails
|
|
59
|
+
* terminally. Filesystem faults are retryable.
|
|
60
|
+
*/
|
|
61
|
+
export async function reconcileSelectedRepositoryInstructions(input: {
|
|
62
|
+
provider: AgentProviderId
|
|
63
|
+
path: string
|
|
64
|
+
repositoryRoots: readonly string[]
|
|
65
|
+
sharedContext?: string
|
|
66
|
+
}): Promise<void> {
|
|
67
|
+
let ownership: ManagedFileOwnership
|
|
68
|
+
try {
|
|
69
|
+
ownership = await readManagedFileOwnership(input.path)
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (!(error instanceof AgentProviderRuntimeError))
|
|
73
|
+
throw error
|
|
74
|
+
throw repositoryInstructionsUnavailable({ recoveryKind: 'retry', cause: error })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const content = selectedRepositoryInstructions(input)
|
|
78
|
+
if (content === undefined) {
|
|
79
|
+
// Only a stale managed file is removed; an unowned one is never touched.
|
|
80
|
+
if (ownership === 'managed')
|
|
81
|
+
await removeManagedFile(input.path)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (ownership === 'unowned') {
|
|
86
|
+
if (input.repositoryRoots.length === 0)
|
|
87
|
+
return
|
|
88
|
+
const cause = new AgentProviderRuntimeError({
|
|
89
|
+
message: `Refusing to overwrite ${basename(input.path)}: it is not owned by the Coding Agent runtime.`,
|
|
90
|
+
details: { instructionBridgePath: input.path },
|
|
91
|
+
retryable: false,
|
|
92
|
+
})
|
|
93
|
+
throw repositoryInstructionsUnavailable({ recoveryKind: 'terminal', cause })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await writeManagedFileAtomically({
|
|
98
|
+
path: input.path,
|
|
99
|
+
content: `${managedInstructionsSentinel}\n${content}`,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (!(error instanceof AgentProviderRuntimeError))
|
|
104
|
+
throw error
|
|
105
|
+
throw repositoryInstructionsUnavailable({ recoveryKind: 'retry', cause: error })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function repositoryInstructionsUnavailable(input: {
|
|
110
|
+
recoveryKind: 'retry' | 'terminal'
|
|
111
|
+
cause: AgentProviderRuntimeError
|
|
112
|
+
}): AgentNotAcceptedError {
|
|
113
|
+
return new AgentNotAcceptedError({
|
|
114
|
+
message: 'Selected repository instructions are unavailable.',
|
|
115
|
+
recovery: { kind: input.recoveryKind, reason: 'repository_instructions_unavailable' },
|
|
116
|
+
cause: input.cause,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function removeManagedFile(path: string): Promise<void> {
|
|
121
|
+
try {
|
|
122
|
+
await unlink(path)
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (isNodeError(error) && error.code === 'ENOENT')
|
|
126
|
+
return
|
|
127
|
+
const cause = new AgentProviderRuntimeError({
|
|
128
|
+
message: 'Failed to remove the stale selected-repository instruction bridge.',
|
|
129
|
+
details: { instructionBridgePath: path },
|
|
130
|
+
retryable: true,
|
|
131
|
+
cause: error,
|
|
132
|
+
})
|
|
133
|
+
throw repositoryInstructionsUnavailable({ recoveryKind: 'retry', cause })
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function readManagedFileOwnership(path: string): Promise<ManagedFileOwnership> {
|
|
138
|
+
let entry
|
|
139
|
+
try {
|
|
140
|
+
entry = await lstat(path)
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (isNodeError(error) && error.code === 'ENOENT')
|
|
144
|
+
return 'missing'
|
|
145
|
+
throw new AgentProviderRuntimeError({
|
|
146
|
+
message: 'Failed to inspect the selected-repository instruction bridge.',
|
|
147
|
+
details: { instructionBridgePath: path },
|
|
148
|
+
retryable: true,
|
|
149
|
+
cause: error,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!entry.isFile() || entry.isSymbolicLink())
|
|
154
|
+
return 'unowned'
|
|
155
|
+
|
|
156
|
+
let content: string
|
|
157
|
+
try {
|
|
158
|
+
content = await readFile(path, 'utf8')
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
throw new AgentProviderRuntimeError({
|
|
162
|
+
message: 'Failed to inspect the selected-repository instruction bridge.',
|
|
163
|
+
details: { instructionBridgePath: path },
|
|
164
|
+
retryable: true,
|
|
165
|
+
cause: error,
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return firstLine(content) === managedInstructionsSentinel ? 'managed' : 'unowned'
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function writeManagedFileAtomically(input: { path: string, content: string }): Promise<void> {
|
|
173
|
+
const tempPath = join(dirname(input.path), `.${basename(input.path)}.${process.pid}.${randomUUID()}.tmp`)
|
|
174
|
+
let tempFile: Awaited<ReturnType<typeof open>> | undefined
|
|
175
|
+
try {
|
|
176
|
+
tempFile = await open(tempPath, 'wx', 0o640)
|
|
177
|
+
await tempFile.chmod(0o640)
|
|
178
|
+
await tempFile.writeFile(input.content, 'utf8')
|
|
179
|
+
await tempFile.close()
|
|
180
|
+
tempFile = undefined
|
|
181
|
+
await rename(tempPath, input.path)
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
await tempFile?.close().catch(() => undefined)
|
|
185
|
+
await unlink(tempPath).catch(() => undefined)
|
|
186
|
+
throw new AgentProviderRuntimeError({
|
|
187
|
+
message: 'Failed to publish the selected-repository instruction bridge.',
|
|
188
|
+
details: { instructionBridgePath: input.path },
|
|
189
|
+
retryable: true,
|
|
190
|
+
cause: error,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function firstLine(content: string): string {
|
|
196
|
+
const lineEnd = content.indexOf('\n')
|
|
197
|
+
return lineEnd === -1 ? content : content.slice(0, lineEnd)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
201
|
+
return error instanceof Error && 'code' in error
|
|
202
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
|
|
3
|
+
/** Product text for a session's web page, delivered once inside the session-context block. */
|
|
4
|
+
export type SessionContextComposer = (input: { sessionWebUrl: string }) => string
|
|
5
|
+
|
|
6
|
+
const openTag = '<coding-agent-session-context>'
|
|
7
|
+
const closeTag = '</coding-agent-session-context>'
|
|
8
|
+
const sessionContextPattern = /^<coding-agent-session-context>[\s\S]*?<\/coding-agent-session-context>\n*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The per-session paragraph (session page URL and MCP access recovery) is delivered once, in the
|
|
12
|
+
* session's first user message, instead of the shared instructions, so the instruction block stays
|
|
13
|
+
* byte-identical across sessions and the provider prompt cache can reuse it.
|
|
14
|
+
*/
|
|
15
|
+
export function sessionContextText(compose: SessionContextComposer, sessionWebUrl: string): string {
|
|
16
|
+
return `${openTag}\n${compose({ sessionWebUrl })}\n${closeTag}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Removes the leading session-context block from echoed user text. */
|
|
20
|
+
export function stripSessionContext(text: string): string {
|
|
21
|
+
return text.replace(sessionContextPattern, '')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The session context a run still has to deliver. A new session owes it to its first user message;
|
|
26
|
+
* a resumed one owes it only when its transcript lacks it (the first message may not have been
|
|
27
|
+
* delivered before the process stopped).
|
|
28
|
+
*/
|
|
29
|
+
export async function pendingSessionContext(input: {
|
|
30
|
+
compose: SessionContextComposer | undefined
|
|
31
|
+
sessionWebUrl: string | undefined
|
|
32
|
+
resumedTranscriptPath: string | undefined
|
|
33
|
+
}): Promise<string | undefined> {
|
|
34
|
+
if (!input.compose || !input.sessionWebUrl)
|
|
35
|
+
return undefined
|
|
36
|
+
const text = sessionContextText(input.compose, input.sessionWebUrl)
|
|
37
|
+
if (input.resumedTranscriptPath === undefined)
|
|
38
|
+
return text
|
|
39
|
+
const transcript = await readFile(input.resumedTranscriptPath, 'utf8').catch(() => '')
|
|
40
|
+
return transcript.includes(input.sessionWebUrl) ? undefined : text
|
|
41
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export interface SubscriptionTokenIdentity {
|
|
2
|
+
accessTokenGeneration: number
|
|
3
|
+
accessTokenFingerprint: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface RejectedSubscriptionToken {
|
|
7
|
+
rejectedTokenGeneration: number
|
|
8
|
+
rejectedTokenFingerprint: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SubscriptionTokenRefreshInput<Tokens extends SubscriptionTokenIdentity> {
|
|
12
|
+
/** The installed tokens a rejection is compared against. */
|
|
13
|
+
installed: () => Promise<Tokens>
|
|
14
|
+
/** The installed tokens after the central refresh returns; throws when the holder has closed. */
|
|
15
|
+
current: () => Tokens
|
|
16
|
+
install: (tokens: Tokens) => void
|
|
17
|
+
refresh: (rejected: RejectedSubscriptionToken) => Promise<Tokens>
|
|
18
|
+
accountId: (tokens: Tokens) => string
|
|
19
|
+
conflictingIdentity: () => Error
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Central subscription-token refresh shared by Codex and Claude credential holders. Concurrent
|
|
24
|
+
* rejections of one token share one refresh; decisions are serialized across accounts because
|
|
25
|
+
* generations are not comparable between them. A rejection of an already-replaced token receives
|
|
26
|
+
* the replacement instead of a new refresh. A replacement is installed when it switches account or
|
|
27
|
+
* does not move the generation backwards; the same generation with a different fingerprint is a
|
|
28
|
+
* conflicting identity.
|
|
29
|
+
*/
|
|
30
|
+
export class SubscriptionTokenRefresh<Tokens extends SubscriptionTokenIdentity> {
|
|
31
|
+
private readonly pending = new Map<string, Promise<Tokens>>()
|
|
32
|
+
private queue: Promise<void> = Promise.resolve()
|
|
33
|
+
|
|
34
|
+
constructor(private readonly input: SubscriptionTokenRefreshInput<Tokens>) {}
|
|
35
|
+
|
|
36
|
+
async refresh(rejected: RejectedSubscriptionToken): Promise<Tokens> {
|
|
37
|
+
const key = `${rejected.rejectedTokenGeneration}:${rejected.rejectedTokenFingerprint}`
|
|
38
|
+
const pending = this.pending.get(key)
|
|
39
|
+
if (pending)
|
|
40
|
+
return await pending
|
|
41
|
+
const refresh = this.queue.then(async () => await this.replace(rejected))
|
|
42
|
+
this.queue = refresh.then(() => undefined, () => undefined)
|
|
43
|
+
this.pending.set(key, refresh)
|
|
44
|
+
try {
|
|
45
|
+
return await refresh
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
this.pending.delete(key)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
clear(): void {
|
|
53
|
+
this.pending.clear()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async replace(rejected: RejectedSubscriptionToken): Promise<Tokens> {
|
|
57
|
+
const installed = await this.input.installed()
|
|
58
|
+
if (rejected.rejectedTokenGeneration !== installed.accessTokenGeneration
|
|
59
|
+
|| rejected.rejectedTokenFingerprint !== installed.accessTokenFingerprint) {
|
|
60
|
+
return installed
|
|
61
|
+
}
|
|
62
|
+
const tokens = await this.input.refresh(rejected)
|
|
63
|
+
const current = this.input.current()
|
|
64
|
+
const sameAccount = this.input.accountId(tokens) === this.input.accountId(current)
|
|
65
|
+
if (sameAccount && tokens.accessTokenGeneration === current.accessTokenGeneration && tokens.accessTokenFingerprint !== current.accessTokenFingerprint)
|
|
66
|
+
throw this.input.conflictingIdentity()
|
|
67
|
+
if (!sameAccount || tokens.accessTokenGeneration >= current.accessTokenGeneration) {
|
|
68
|
+
this.input.install(tokens)
|
|
69
|
+
return tokens
|
|
70
|
+
}
|
|
71
|
+
return current
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -142,7 +142,11 @@ export class UserInputRequestStore {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
shouldHoldIdle(): boolean {
|
|
145
|
-
return
|
|
145
|
+
return this.hasUnresolvedUserInput()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
hasUnresolvedUserInput(): boolean {
|
|
149
|
+
return [...this.requests.values()].some(request => request.status === 'pending' || request.status === 'answered')
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
resolve(command: RespondUserInputAgentCommand): ResolvedUserInputRequest | null {
|
|
@@ -165,10 +169,22 @@ export class UserInputRequestStore {
|
|
|
165
169
|
return null
|
|
166
170
|
|
|
167
171
|
if (record.status === 'answered') {
|
|
168
|
-
|
|
172
|
+
const existingAnswers = record.answers
|
|
173
|
+
if (!existingAnswers) {
|
|
174
|
+
throw new AgentRunStateError({
|
|
175
|
+
message: `User input request ${record.requestId} is answered without stored answers.`,
|
|
176
|
+
details: { requestId: record.requestId },
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
if (!sameAnswerValues(existingAnswers, command.answers)) {
|
|
180
|
+
throw new AgentRunStateError({
|
|
181
|
+
message: `User input request ${record.requestId} was already answered with different answers.`,
|
|
182
|
+
details: { requestId: record.requestId },
|
|
183
|
+
})
|
|
184
|
+
}
|
|
169
185
|
return {
|
|
170
186
|
record,
|
|
171
|
-
prompt: promptForResolvedUserInput(record,
|
|
187
|
+
prompt: promptForResolvedUserInput(record, existingAnswers),
|
|
172
188
|
}
|
|
173
189
|
}
|
|
174
190
|
|
|
@@ -399,23 +415,6 @@ function validateAnswers(record: UserInputRequestRecord, answers: AgentUserInput
|
|
|
399
415
|
}
|
|
400
416
|
}
|
|
401
417
|
|
|
402
|
-
function assertSameAnswers(record: UserInputRequestRecord, answers: AgentUserInputAnswers): asserts record is UserInputRequestRecord & { answers: AgentUserInputAnswers } {
|
|
403
|
-
const existing = record.answers
|
|
404
|
-
if (!existing) {
|
|
405
|
-
throw new AgentRunStateError({
|
|
406
|
-
message: `User input request ${record.requestId} is answered without stored answers.`,
|
|
407
|
-
details: { requestId: record.requestId },
|
|
408
|
-
})
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (!sameAnswerValues(existing, answers)) {
|
|
412
|
-
throw new AgentRunStateError({
|
|
413
|
-
message: `User input request ${record.requestId} was already answered with different answers.`,
|
|
414
|
-
details: { requestId: record.requestId },
|
|
415
|
-
})
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
418
|
function sameAnswerValues(left: AgentUserInputAnswers, right: AgentUserInputAnswers): boolean {
|
|
420
419
|
return stableJson(left) === stableJson(right)
|
|
421
420
|
}
|
package/src/errors/index.ts
CHANGED
|
@@ -12,5 +12,5 @@ export { InfrastructureError } from './infrastructure-error'
|
|
|
12
12
|
export type { InfrastructureErrorInput } from './infrastructure-error'
|
|
13
13
|
export { ProviderError } from './provider-error'
|
|
14
14
|
export type { ProviderErrorInput } from './provider-error'
|
|
15
|
-
export { UnprocessableEntityError, ValidationError } from './validation-error'
|
|
15
|
+
export { PayloadTooLargeError, UnprocessableEntityError, ValidationError } from './validation-error'
|
|
16
16
|
export type { ValidationErrorInput } from './validation-error'
|