mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-09 04:02:35 +00:00
refactor(file-upload): scope prompt binding rollback
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/attachment.md
|
||||
attachment.md: 3fe72b70af27a7597bca8eed24070589c1ba64d7
|
||||
attachment.zh.md: db43180565baaf0512cd5dc261324c79dddd1552
|
||||
attachment.md: 6d9d4c86aa9435450a766c6aa74f4062824ba96b
|
||||
attachment.zh.md: 4a8a7c3acf4203ed4d3cff2b92098086a5aa39a9
|
||||
|
||||
@@ -305,13 +305,14 @@ async uploadStream(request: { readonly sessionId: SessionId readonly data: Async
|
||||
resolve(agent: Agent, receiptId: FileUploadReceiptId): FileAttachmentRef | undefined
|
||||
|
||||
/**
|
||||
* Bind receipts to an accepted prompt and return a rollback for delivery failure.
|
||||
* Bind receipts while one prompt enters an Agent inbox.
|
||||
* Disposal restores every prior binding unless the caller commits successful delivery.
|
||||
* @param agent - receiving Agent.
|
||||
* @param receiptIds - distinct staged receipts referenced by the prompt.
|
||||
* @param requestId - prompt identity later observed in queue or history.
|
||||
* @returns rollback restoring every prior binding.
|
||||
* @returns binding kept after commit until queue or history observation retires its receipts.
|
||||
*/
|
||||
bindPrompt(agent: Agent, receiptIds: readonly FileUploadReceiptId[], requestId: string): () => void
|
||||
bindPrompt( agent: Agent, receiptIds: readonly FileUploadReceiptId[], requestId: string, ): PromptFileBinding
|
||||
|
||||
/**
|
||||
* Retire every receipt accepted by one removed queue occurrence.
|
||||
|
||||
@@ -305,13 +305,14 @@ async uploadStream(request: { readonly sessionId: SessionId readonly data: Async
|
||||
resolve(agent: Agent, receiptId: FileUploadReceiptId): FileAttachmentRef | undefined
|
||||
|
||||
/**
|
||||
* Bind receipts to an accepted prompt and return a rollback for delivery failure.
|
||||
* Bind receipts while one prompt enters an Agent inbox.
|
||||
* Disposal restores every prior binding unless the caller commits successful delivery.
|
||||
* @param agent - receiving Agent.
|
||||
* @param receiptIds - distinct staged receipts referenced by the prompt.
|
||||
* @param requestId - prompt identity later observed in queue or history.
|
||||
* @returns rollback restoring every prior binding.
|
||||
* @returns binding kept after commit until queue or history observation retires its receipts.
|
||||
*/
|
||||
bindPrompt(agent: Agent, receiptIds: readonly FileUploadReceiptId[], requestId: string): () => void
|
||||
bindPrompt( agent: Agent, receiptIds: readonly FileUploadReceiptId[], requestId: string, ): PromptFileBinding
|
||||
|
||||
/**
|
||||
* Retire every receipt accepted by one removed queue occurrence.
|
||||
|
||||
@@ -341,14 +341,10 @@ export class SessionCommandController {
|
||||
{ sessionId: agent.id },
|
||||
)
|
||||
}
|
||||
const rollback = this.ctx.fileUploads.bindPrompt(agent, durable.receiptIds, request.requestId)
|
||||
try {
|
||||
if (request.mode === 'steer') agent.steer(message)
|
||||
else agent.followup(message)
|
||||
} catch (error) {
|
||||
rollback()
|
||||
throw error
|
||||
}
|
||||
using binding = this.ctx.fileUploads.bindPrompt(agent, durable.receiptIds, request.requestId)
|
||||
if (request.mode === 'steer') agent.steer(message)
|
||||
else agent.followup(message)
|
||||
binding.commit()
|
||||
} catch (error) {
|
||||
if (remoteErrorOf(error) !== undefined) throw error
|
||||
if (error instanceof AttachmentError) {
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('SessionController facade', () => {
|
||||
return () => {}
|
||||
},
|
||||
resolve: () => undefined,
|
||||
bindPrompt: () => () => {},
|
||||
bindPrompt: () => ({ commit: () => {}, [Symbol.dispose]: () => {} }),
|
||||
retirePrompt: () => {},
|
||||
} as never)
|
||||
const controller = createSessionTestController(ctx, defaults)
|
||||
|
||||
@@ -240,7 +240,7 @@ function installControllers(
|
||||
ctx.provide('fileUploads', {
|
||||
registerAgentResolver: () => () => {},
|
||||
resolve: () => undefined,
|
||||
bindPrompt: () => () => {},
|
||||
bindPrompt: () => ({ commit: () => {}, [Symbol.dispose]: () => {} }),
|
||||
retirePrompt: () => {},
|
||||
} as never)
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/client/file-upload/README.md
|
||||
README.md: be7f923fc7769bb337597a53a82b22be73866b8f
|
||||
README.zh.md: fb355562c1249f85585cef4cbbaaa674a8ced690
|
||||
README.md: 5527bf6f544529088a255934a87af77be4865e12
|
||||
README.zh.md: 7825326863511cd7098635d563237bddeb46d035
|
||||
|
||||
@@ -44,7 +44,7 @@ The package has no Cordis configuration fields. A `Blob` uses XMLHttpRequest ins
|
||||
|
||||
The Client plugin provides `ctx.fileUpload`. Its `upload()` method asks Typert's registered `agent` Context adapter for the caller's identity, assembles the raw route request, and invokes the generated scoped Remote fallback for replayable inputs. The provider reads the optional pre-Cordis `__DSH_FILE_UPLOAD__` hook once. Without a hook, each non-fixture raw request owns a short-lived Worker and releases it after completion, failure, or cancellation. With the hook, the service sends the body through the page-owned Fetch carrier; the Web Worker runtime transfers stream bodies through its request frame and exposes them to the Host HTTP bridge as backpressured chunks.
|
||||
|
||||
The Host plugin provides `ctx.fileUploads`. It owns the authenticated streaming route, encoded Remote fallback, byte storage, command receipt resolver, and staged-receipt lifecycle. Receipt tables use the receiving Agent's Session object as their key. The Session Controller registers the resolver that can resume a cold ordinary Agent and consumes receipts during prompt admission.
|
||||
The Host plugin provides `ctx.fileUploads`. It owns the authenticated streaming route, encoded Remote fallback, byte storage, command receipt resolver, and staged-receipt lifecycle. Receipt tables use the receiving Agent's Session object as their key. The Session Controller registers the resolver that can resume a cold ordinary Agent and consumes receipts during prompt admission. Prompt delivery holds each receipt binding in a disposable transaction: disposal restores the previous binding until successful delivery commits it, and queue or history observation then retires the committed receipt.
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
|
||||
@@ -44,7 +44,7 @@ kind: "package-reference"
|
||||
|
||||
Client 插件提供 `ctx.fileUpload`。其 `upload()` 方法通过 Typert 已注册的 `agent` Context adapter 取得调用方身份,组装原始路由请求,并为可重放输入调用生成的按 scope 寻址 Remote 兜底。提供方只读取一次可选的 Cordis 启动前 `__DSH_FILE_UPLOAD__` 钩子。没有该钩子时,每个非 fixture 原始请求拥有一个短期 Worker,并在完成、失败或取消后释放。存在该钩子时,服务通过页面自己提供的 Fetch 载体发送请求体;Web Worker runtime 会通过请求帧转移 stream 请求体,再以带背压的分片形式交给 Host HTTP bridge。
|
||||
|
||||
Host 插件提供 `ctx.fileUploads`。它拥有经过认证的流式路由、编码 Remote 兜底、字节存储、命令凭证解析器与暂存凭证生命周期。凭证表以接收方 Agent 的 Session 对象为键。Session Controller 注册可恢复休眠普通 Agent 的解析器,并在 prompt 准入时消费凭证。
|
||||
Host 插件提供 `ctx.fileUploads`。它拥有经过认证的流式路由、编码 Remote 兜底、字节存储、命令凭证解析器与暂存凭证生命周期。凭证表以接收方 Agent 的 Session 对象为键。Session Controller 注册可恢复休眠普通 Agent 的解析器,并在 prompt 准入时消费凭证。Prompt 投递通过可释放事务持有每个凭证绑定。成功投递提交事务前,释放会恢复原绑定;提交后,队列或历史观察会退休该凭证。
|
||||
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
|
||||
@@ -33,6 +33,12 @@ interface StagedFileUpload {
|
||||
/** Resolve or resume the ordinary Agent that owns one Session identity. */
|
||||
export type AgentResolver = (sessionId: SessionId) => Promise<Agent>
|
||||
|
||||
/** Prompt receipt binding that restores its previous owners unless delivery commits it. */
|
||||
export interface PromptFileBinding extends Disposable {
|
||||
/** Keep the receipt bindings until queue or history observation retires them. */
|
||||
commit(): void
|
||||
}
|
||||
|
||||
type FileUploadHttpResult =
|
||||
| { readonly ok: true; readonly value: FileUploadValue }
|
||||
| {
|
||||
@@ -40,6 +46,22 @@ type FileUploadHttpResult =
|
||||
readonly error: { readonly code: string; readonly message: string; readonly details: object }
|
||||
}
|
||||
|
||||
class PromptFileBindingGuard implements PromptFileBinding {
|
||||
private settled = false
|
||||
|
||||
constructor(private readonly rollback: () => void) {}
|
||||
|
||||
commit(): void {
|
||||
this.settled = true
|
||||
}
|
||||
|
||||
[Symbol.dispose](): void {
|
||||
if (this.settled) return
|
||||
this.settled = true
|
||||
this.rollback()
|
||||
}
|
||||
}
|
||||
|
||||
/** Host service owning upload storage and Agent-scoped staged receipts. */
|
||||
export class FileUploads extends TypertRemoteService {
|
||||
static inject = ['agents', 'attachments', 'commands', 'connection']
|
||||
@@ -129,13 +151,18 @@ export class FileUploads extends TypertRemoteService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind receipts to an accepted prompt and return a rollback for delivery failure.
|
||||
* Bind receipts while one prompt enters an Agent inbox.
|
||||
* Disposal restores every prior binding unless the caller commits successful delivery.
|
||||
* @param agent - receiving Agent.
|
||||
* @param receiptIds - distinct staged receipts referenced by the prompt.
|
||||
* @param requestId - prompt identity later observed in queue or history.
|
||||
* @returns rollback restoring every prior binding.
|
||||
* @returns binding kept after commit until queue or history observation retires its receipts.
|
||||
*/
|
||||
bindPrompt(agent: Agent, receiptIds: readonly FileUploadReceiptId[], requestId: string): () => void {
|
||||
bindPrompt(
|
||||
agent: Agent,
|
||||
receiptIds: readonly FileUploadReceiptId[],
|
||||
requestId: string,
|
||||
): PromptFileBinding {
|
||||
this.assertAgentScope(agent)
|
||||
const staged = this.stagedFiles.get(agent.session)
|
||||
const bound = receiptIds.map((receiptId) => {
|
||||
@@ -144,12 +171,12 @@ export class FileUploads extends TypertRemoteService {
|
||||
return { upload, previous: upload.requestId }
|
||||
})
|
||||
for (const { upload } of bound) upload.requestId = requestId
|
||||
return () => {
|
||||
return new PromptFileBindingGuard(() => {
|
||||
for (const { upload, previous } of bound) {
|
||||
if (previous === undefined) delete upload.requestId
|
||||
else upload.requestId = previous
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -918,10 +918,10 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
returns: 'durable file reference, or `undefined` for an unknown or foreign receipt.',
|
||||
},
|
||||
{
|
||||
signature: 'bindPrompt(agent: Agent, receiptIds: readonly FileUploadReceiptId[], requestId: string): () => void',
|
||||
description: 'Bind receipts to an accepted prompt and return a rollback for delivery failure.',
|
||||
signature: 'bindPrompt( agent: Agent, receiptIds: readonly FileUploadReceiptId[], requestId: string, ): PromptFileBinding',
|
||||
description: 'Bind receipts while one prompt enters an Agent inbox. Disposal restores every prior binding unless the caller commits successful delivery.',
|
||||
parameters: [{ name: 'agent', description: 'receiving Agent.' }, { name: 'receiptIds', description: 'distinct staged receipts referenced by the prompt.' }, { name: 'requestId', description: 'prompt identity later observed in queue or history.' }],
|
||||
returns: 'rollback restoring every prior binding.',
|
||||
returns: 'binding kept after commit until queue or history observation retires its receipts.',
|
||||
},
|
||||
{
|
||||
signature: 'retirePrompt(agent: Agent, requestId: string): void',
|
||||
@@ -4678,6 +4678,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'PromptContextOrderName',
|
||||
declaration: 'export type PromptContextOrderName = keyof typeof CONTEXT_ORDERS;',
|
||||
},
|
||||
{
|
||||
name: 'PromptFileBinding',
|
||||
declaration: 'export interface PromptFileBinding extends Disposable {\n commit(): void;\n}',
|
||||
},
|
||||
{
|
||||
name: 'PromptSection',
|
||||
declaration: 'export interface PromptSection {\n readonly name: string;\n readonly order: number;\n readonly text: string | ((context: AssembleContext) => string);\n readonly complete?: boolean;\n}',
|
||||
|
||||
@@ -346,6 +346,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
SessionUpdateQueueValue: 'session.md',
|
||||
EncodedFileUploadRequest: 'attachment.md',
|
||||
AgentResolver: 'attachment.md',
|
||||
PromptFileBinding: 'attachment.md',
|
||||
FileUploadReceiptId: 'attachment.md',
|
||||
FileUploadValue: 'attachment.md',
|
||||
SessionStartSource: 'core.md',
|
||||
|
||||
Reference in New Issue
Block a user