@springbrand/agent-runtime 0.1.3-alpha.8 → 0.2.0-alpha.13

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.
Files changed (40) hide show
  1. package/package.json +4 -3
  2. package/src/adapter/cloudflare/index.ts +1 -0
  3. package/src/adapter/cloudflare/sandbox/adapter.ts +4 -0
  4. package/src/adapter/cloudflare/subagent/definition.ts +60 -5
  5. package/src/adapter/cloudflare/universal-agent/hooks.ts +23 -1
  6. package/src/adapter/cloudflare/universal-agent/preparation.ts +34 -0
  7. package/src/adapter/cloudflare/universal-agent/tools.ts +7 -7
  8. package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
  9. package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
  10. package/src/db/index.ts +4 -0
  11. package/src/db/runtime-event-outbox.repo.ts +34 -1
  12. package/src/db/schema.ts +42 -0
  13. package/src/db/submission-admission.repo.ts +127 -0
  14. package/src/db/submission.repo.ts +54 -3
  15. package/src/index.ts +3 -0
  16. package/src/kernel/bindings.ts +52 -0
  17. package/src/kernel/durable-lifecycle.ts +100 -0
  18. package/src/kernel/public-contracts.ts +11 -0
  19. package/src/kernel/receipts.ts +1 -0
  20. package/src/kernel/recoverable-chat-agent.ts +0 -19
  21. package/src/kernel/subagent-runtime.ts +137 -0
  22. package/src/kernel/submission-authority.ts +114 -0
  23. package/src/kernel/submission-lifecycle.ts +17 -5
  24. package/src/lib/prompt.ts +3 -0
  25. package/src/pi/runtime-adapter/assembly.ts +2 -1
  26. package/src/pi/runtime-adapter/execution.ts +8 -1
  27. package/src/pi/runtime-adapter/index.ts +33 -0
  28. package/src/pi/runtime-adapter/models.ts +9 -0
  29. package/src/pi/tool/core-host.ts +5 -1
  30. package/src/pi/tool/core.ts +1 -10
  31. package/src/pi/tool/index.ts +1 -0
  32. package/src/pi/tool/mcp.ts +2 -2
  33. package/src/pi/tool/subagent.ts +142 -16
  34. package/src/pi/tool/workspace-revision.ts +64 -0
  35. package/src/runtime-agent-context.ts +1 -0
  36. package/src/runtime-assembler.ts +16 -1
  37. package/src/runtime-definition.ts +12 -0
  38. package/src/runtime.ts +427 -46
  39. package/src/tool-registry.ts +2 -0
  40. package/src/workspace-versioning.ts +46 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.1.3-alpha.8",
3
+ "version": "0.2.0-alpha.13",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -22,15 +22,16 @@
22
22
  "@cloudflare/think": "0.15.0",
23
23
  "@earendil-works/pi-agent-core": "0.83.0",
24
24
  "@earendil-works/pi-ai": "0.83.0",
25
+ "isomorphic-git": "1.38.6",
25
26
  "lodash-es": "^4.18.1",
26
27
  "zod": "^4.4.3"
27
28
  },
28
29
  "peerDependencies": {
29
- "agents": "^0.19.0",
30
+ "agents": "^0.20.1",
30
31
  "ai": "^7.0.0"
31
32
  },
32
33
  "devDependencies": {
33
- "agents": "0.19.0",
34
+ "agents": "0.20.1",
34
35
  "ai": "7.0.37"
35
36
  }
36
37
  }
@@ -31,6 +31,7 @@ export {
31
31
  createWorkspacePortFacade,
32
32
  ScopedWorkspace,
33
33
  } from "./workspace/scoped-workspace";
34
+ export { WorkspaceVersionControl } from "./workspace/version-control";
34
35
  export type {
35
36
  WorkspaceConditionalWriteResult,
36
37
  WorkspaceFileVersion,
@@ -155,6 +155,7 @@ export interface CloudflareSandboxAdapterOptions {
155
155
  sandboxId: string;
156
156
  workspace: WorkspacePort;
157
157
  publisher: WorkspacePublishPort;
158
+ onPublishComplete?: () => Promise<void>;
158
159
  admission?: SandboxAdmission;
159
160
  observer?: SandboxObserver;
160
161
  clientFactory?: SandboxClientFactory;
@@ -829,6 +830,9 @@ export class CloudflareSandboxAdapter implements RuntimeSandboxPort {
829
830
  await this.writeHydrationManifest(manifest);
830
831
  }
831
832
  const frozen = this.freezeSyncResult(result);
833
+ if (frozen.files.length > 0 && frozen.failed.length === 0) {
834
+ await this.options.onPublishComplete?.();
835
+ }
832
836
  this.emit("sandbox.workspace.published", {
833
837
  success: frozen.failed.length === 0,
834
838
  files: frozen.files.length,
@@ -24,6 +24,13 @@ import {
24
24
  withProviderRetry,
25
25
  } from "../../../pi/runtime-adapter/models";
26
26
  import { runCloudflareSubAgent } from "./runner";
27
+ import {
28
+ addSubagentUsage,
29
+ decodeSubagentCompletion,
30
+ emptySubagentUsage,
31
+ encodeSubagentCompletion,
32
+ } from "../../../kernel/subagent-runtime";
33
+ import type { RuntimeSubagentUsageMetrics } from "../../../kernel/submission-authority";
27
34
  import { createCloudflareSubAgentTools } from "./tools";
28
35
 
29
36
  type RunStatus =
@@ -44,6 +51,7 @@ interface RunRow {
44
51
  started_at: number;
45
52
  completed_at: number | null;
46
53
  progress_json: string | null;
54
+ usage_json: string | null;
47
55
  }
48
56
 
49
57
  interface ChunkRow {
@@ -148,9 +156,17 @@ export abstract class CloudflareSubAgent<
148
156
  error_message TEXT,
149
157
  started_at INTEGER NOT NULL,
150
158
  completed_at INTEGER,
151
- progress_json TEXT
159
+ progress_json TEXT,
160
+ usage_json TEXT
152
161
  )
153
162
  `;
163
+ const runColumns = new Set(
164
+ this.sql<{ name: string }>`PRAGMA table_info(pi_subagent_runs)`
165
+ .map((column) => column.name),
166
+ );
167
+ if (!runColumns.has("usage_json")) {
168
+ this.sql`ALTER TABLE pi_subagent_runs ADD COLUMN usage_json TEXT`;
169
+ }
154
170
  this.sql`
155
171
  CREATE TABLE IF NOT EXISTS pi_subagent_chunks (
156
172
  run_id TEXT NOT NULL,
@@ -182,7 +198,8 @@ export abstract class CloudflareSubAgent<
182
198
  error_message,
183
199
  started_at,
184
200
  completed_at,
185
- progress_json
201
+ progress_json,
202
+ usage_json
186
203
  FROM pi_subagent_runs
187
204
  WHERE run_id = ${runId}
188
205
  `[0] ?? null
@@ -240,6 +257,26 @@ export abstract class CloudflareSubAgent<
240
257
  }
241
258
  }
242
259
 
260
+ private readUsage(runId: string): RuntimeSubagentUsageMetrics {
261
+ const value = this.readRun(runId)?.usage_json;
262
+ if (!value) return emptySubagentUsage();
263
+ const decoded = decodeSubagentCompletion(
264
+ stringifyJson({ usage: parseJson(value) }),
265
+ );
266
+ return decoded?.usage ?? emptySubagentUsage();
267
+ }
268
+
269
+ private persistUsage(
270
+ runId: string,
271
+ usage: RuntimeSubagentUsageMetrics,
272
+ ): void {
273
+ this.sql`
274
+ UPDATE pi_subagent_runs
275
+ SET usage_json = ${stringifyJson(usage)}
276
+ WHERE run_id = ${runId}
277
+ `;
278
+ }
279
+
243
280
  private persistProgress(
244
281
  runId: string,
245
282
  progress: Omit<AgentToolProgressSnapshot, "at">,
@@ -269,14 +306,20 @@ export abstract class CloudflareSubAgent<
269
306
  for (const subscriber of subscribers) subscriber.close();
270
307
  }
271
308
 
272
- private completeRun(runId: string, output: unknown): void {
309
+ private completeRun(
310
+ runId: string,
311
+ output: unknown,
312
+ usage: RuntimeSubagentUsageMetrics,
313
+ ): void {
273
314
  const outputJson = stringifyJson(output);
315
+ const summary = encodeSubagentCompletion(output, usage);
274
316
  this.sql`
275
317
  UPDATE pi_subagent_runs
276
318
  SET
277
319
  status = 'completed',
278
320
  output_json = ${outputJson},
279
- summary = ${outputJson},
321
+ summary = ${summary},
322
+ usage_json = ${stringifyJson(usage)},
280
323
  error_message = NULL,
281
324
  completed_at = ${Date.now()}
282
325
  WHERE run_id = ${runId}
@@ -289,11 +332,15 @@ export abstract class CloudflareSubAgent<
289
332
  runId: string,
290
333
  status: "error" | "aborted",
291
334
  error: string,
335
+ usage: RuntimeSubagentUsageMetrics = this.readUsage(runId),
292
336
  ): void {
337
+ const summary = encodeSubagentCompletion(undefined, usage);
293
338
  this.sql`
294
339
  UPDATE pi_subagent_runs
295
340
  SET
296
341
  status = ${status},
342
+ summary = ${summary},
343
+ usage_json = ${stringifyJson(usage)},
297
344
  error_message = ${error},
298
345
  completed_at = ${Date.now()}
299
346
  WHERE run_id = ${runId}
@@ -315,6 +362,7 @@ export abstract class CloudflareSubAgent<
315
362
  WHERE run_id = ${runId} AND status = 'starting'
316
363
  `;
317
364
 
365
+ const usage = this.readUsage(runId);
318
366
  try {
319
367
  const { provider, workspace, loader, outbound } =
320
368
  await this.prepareExecution();
@@ -355,6 +403,10 @@ export abstract class CloudflareSubAgent<
355
403
  tools,
356
404
  signal,
357
405
  onEvent: (event) => {
406
+ if (event.type === "message_end" && event.message.role === "assistant") {
407
+ addSubagentUsage(usage, event.message.usage);
408
+ this.persistUsage(runId, usage);
409
+ }
358
410
  for (const chunk of encoder.encode(event)) {
359
411
  this.appendChunk(runId, stringifyJson({
360
412
  v: 1,
@@ -367,12 +419,13 @@ export abstract class CloudflareSubAgent<
367
419
  if (progress) this.persistProgress(runId, progress);
368
420
  },
369
421
  });
370
- this.completeRun(runId, output);
422
+ this.completeRun(runId, output, usage);
371
423
  } catch (error) {
372
424
  this.failRun(
373
425
  runId,
374
426
  signal.aborted ? "aborted" : "error",
375
427
  signal.aborted ? "SubAgent run aborted" : errorMessage(error),
428
+ usage,
376
429
  );
377
430
  }
378
431
  }
@@ -413,6 +466,7 @@ export abstract class CloudflareSubAgent<
413
466
  agent_type,
414
467
  status,
415
468
  input_json,
469
+ usage_json,
416
470
  started_at
417
471
  )
418
472
  VALUES (
@@ -420,6 +474,7 @@ export abstract class CloudflareSubAgent<
420
474
  ${type.name},
421
475
  'starting',
422
476
  ${inputJson},
477
+ ${stringifyJson(emptySubagentUsage())},
423
478
  ${startedAt}
424
479
  )
425
480
  `;
@@ -1,4 +1,7 @@
1
- import type { RuntimeTurnEventsPort } from "../../../kernel/bindings";
1
+ import type {
2
+ RuntimeSubmissionAdmissionHook,
3
+ RuntimeTurnEventsPort,
4
+ } from "../../../kernel/bindings";
2
5
  import type { RuntimeAgentHooks } from "../../../runtime-definition";
3
6
 
4
7
  export function createUniversalAgentHooks<
@@ -9,6 +12,7 @@ export function createUniversalAgentHooks<
9
12
  >(adapter: {
10
13
  guard(config: Config): Promise<void>;
11
14
  turnEvents(): RuntimeTurnEventsPort | Promise<RuntimeTurnEventsPort>;
15
+ onSubmissionAdmission?: RuntimeSubmissionAdmissionHook;
12
16
  }): RuntimeAgentHooks<Env, Config, Command, Change> {
13
17
  const turnEvents = () => Promise.resolve(adapter.turnEvents());
14
18
  return {
@@ -19,6 +23,24 @@ export function createUniversalAgentHooks<
19
23
  onModelUsage: async (event) => {
20
24
  await (await turnEvents()).onModelUsage?.(event);
21
25
  },
26
+ ...(adapter.onSubmissionAdmission
27
+ ? { onSubmissionAdmission: adapter.onSubmissionAdmission }
28
+ : {}),
29
+ // 一个不订阅这两类事实的 Host 不能把 outbox 堵住:Runtime 只有拿到确认才
30
+ // 会标记投递,而 `undefined` 表示"没投出去"。因此没有 Host 实现时这里就地确认,
31
+ // 语义等同于旧版本"Host 不关心就丢弃",而不是让整条队列在队头无限重试。
32
+ onSubagentUsage: async (event) => {
33
+ const port = await turnEvents();
34
+ return port.onSubagentUsage
35
+ ? port.onSubagentUsage(event)
36
+ : { confirmed: true };
37
+ },
38
+ onLifecycleFact: async (event) => {
39
+ const port = await turnEvents();
40
+ return port.onLifecycleFact
41
+ ? port.onLifecycleFact(event)
42
+ : { confirmed: true };
43
+ },
22
44
  onToolSettled: async (event) => {
23
45
  await (await turnEvents()).onToolSettled?.(event);
24
46
  },
@@ -10,6 +10,7 @@ import type {
10
10
  RuntimeSubagentPort,
11
11
  WorkspacePort,
12
12
  } from "../../../kernel/bindings";
13
+ import { decodeSubagentCompletion } from "../../../kernel/subagent-runtime";
13
14
  import type { RuntimeDegradation } from "../../../kernel/degradation";
14
15
  import type { RuntimeMemoryProfile } from "../../../kernel/profile";
15
16
  import { AGENT_TYPES } from "../../../layers/orchestration/subagents/agent-types/registry";
@@ -193,9 +194,33 @@ export async function prepareSubagents<Env extends Cloudflare.Env>(
193
194
  }
194
195
 
195
196
  const subagents: RuntimeSubagentPort = {
197
+ reserve: async (_agentType, _input, options) => {
198
+ if (!options?.submissionId || !options.toolCallId) {
199
+ throw new Error("Durable SubAgent reservation identity is required");
200
+ }
201
+ return {
202
+ runId: `${options.submissionId}:subagent:${options.toolCallId}`,
203
+ };
204
+ },
196
205
  run: async (agentType, input, options) => {
197
206
  const runOptions: RunAgentToolOptions<unknown> = {
198
207
  input: { agentType, ...input },
208
+ ...(options?.subagentRunId ? { runId: options.subagentRunId } : {}),
209
+ ...(options?.submissionId && options.parentRunId
210
+ ? {
211
+ inputPreview: {
212
+ submissionId: options.submissionId,
213
+ parentRunId: options.parentRunId,
214
+ ...(options.accountId ? { accountId: options.accountId } : {}),
215
+ ...(options.rateVersion !== undefined
216
+ ? { rateVersion: options.rateVersion }
217
+ : {}),
218
+ ...(options.slotIdentity
219
+ ? { slotIdentity: options.slotIdentity }
220
+ : {}),
221
+ },
222
+ }
223
+ : {}),
199
224
  };
200
225
  if (options?.detached) {
201
226
  runOptions.detached = { maxBudgetMs: options.maxBudgetMs };
@@ -204,11 +229,16 @@ export async function prepareSubagents<Env extends Cloudflare.Env>(
204
229
  }
205
230
  }
206
231
  const result = await context.runAgentTool(agentClass, runOptions);
232
+ const completion = decodeSubagentCompletion(result.summary);
207
233
  return {
208
234
  status: result.status,
209
235
  runId: result.runId,
210
236
  output: result.output,
211
237
  error: result.error,
238
+ ...(result.childStillRunning !== undefined
239
+ ? { childStillRunning: result.childStillRunning }
240
+ : {}),
241
+ ...(completion ? { usage: completion.usage } : {}),
212
242
  };
213
243
  },
214
244
  };
@@ -228,6 +258,7 @@ export async function prepareSandbox(options: {
228
258
  sessionId: string;
229
259
  workspaceLoader: WorkspaceLoader;
230
260
  publisher: WorkspacePublishPort;
261
+ onPublishComplete?: () => Promise<void>;
231
262
  admission?: SandboxAdmission;
232
263
  observer?: SandboxObserver;
233
264
  }) {
@@ -265,6 +296,9 @@ export async function prepareSandbox(options: {
265
296
  sandboxId: await sandboxIdForSession(options.userId, options.sessionId),
266
297
  workspace,
267
298
  publisher: options.publisher,
299
+ ...(options.onPublishComplete
300
+ ? { onPublishComplete: options.onPublishComplete }
301
+ : {}),
268
302
  ...(options.admission ? { admission: options.admission } : {}),
269
303
  ...(options.observer ? { observer: options.observer } : {}),
270
304
  }),
@@ -8,9 +8,10 @@ import type {
8
8
  } from "../../../kernel/bindings";
9
9
  import type { RuntimeDegradation } from "../../../kernel/degradation";
10
10
  import type { RuntimeMemoryProfile } from "../../../kernel/profile";
11
+ import type { WorkspaceRevisionRestorePort } from "../../../workspace-versioning";
11
12
  import { createCodeExecutionTool } from "../../../pi/tool/core";
12
13
  import { createScheduleTools } from "../../../pi/tool/schedule";
13
- import { createSubagentTools } from "../../../pi/tool/subagent";
14
+ import { createWorkspaceRevisionTools } from "../../../pi/tool/workspace-revision";
14
15
  import {
15
16
  createSandboxTools,
16
17
  createWorkspaceTools,
@@ -34,6 +35,7 @@ export function selectTools(
34
35
  export function assembleUniversalAgentTools(options: {
35
36
  hostTools?: ToolRegistry;
36
37
  workspace?: WorkspacePort;
38
+ workspaceRevisions?: WorkspaceRevisionRestorePort;
37
39
  codeExecution?: RuntimeCodeExecutionPort;
38
40
  sandbox?: RuntimeSandboxPort;
39
41
  schedule?: RuntimeSchedulePort;
@@ -47,17 +49,14 @@ export function assembleUniversalAgentTools(options: {
47
49
  const tools = mergeToolRegistries(
48
50
  options.hostTools ?? {},
49
51
  options.workspace ? createWorkspaceTools(options.workspace) : {},
52
+ options.workspaceRevisions
53
+ ? createWorkspaceRevisionTools(options.workspaceRevisions)
54
+ : {},
50
55
  options.codeExecution
51
56
  ? createCodeExecutionTool(options.codeExecution)
52
57
  : {},
53
58
  options.sandbox ? createSandboxTools(options.sandbox) : {},
54
59
  options.schedule ? createScheduleTools(options.schedule) : {},
55
- options.subagents
56
- ? createSubagentTools(
57
- options.subagents,
58
- options.enabledSubagents ?? [],
59
- )
60
- : {},
61
60
  );
62
61
 
63
62
  return {
@@ -65,6 +64,7 @@ export function assembleUniversalAgentTools(options: {
65
64
  bindings: {
66
65
  ...(options.workspace ? { workspace: options.workspace } : {}),
67
66
  ...(options.memory ? { memory: options.memory } : {}),
67
+ ...(options.subagents ? { subagents: options.subagents } : {}),
68
68
  },
69
69
  enabledSubagents: options.enabledSubagents ?? [],
70
70
  memoryProfile: options.memoryProfile,
@@ -0,0 +1,178 @@
1
+ /*
2
+ * Adapted from @cloudflare/shell.
3
+ * MIT License Copyright (c) 2025 Cloudflare, Inc.
4
+ *
5
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ * of this software and associated documentation files (the "Software"), to deal
7
+ * in the Software without restriction, including without limitation the rights
8
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ * copies of the Software, and to permit persons to whom the Software is
10
+ * furnished to do so, subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be included in
13
+ * all copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ * SOFTWARE.
22
+ */
23
+ import type {
24
+ FsClient,
25
+ PromiseFsClient,
26
+ } from "isomorphic-git";
27
+ import type {
28
+ WorkspaceFileInfo,
29
+ WorkspacePort,
30
+ } from "../../../kernel/bindings";
31
+
32
+ const PAGE_SIZE = 256;
33
+
34
+ class GitStat {
35
+ readonly size: number;
36
+ readonly mtime: Date;
37
+ readonly mtimeMs: number;
38
+ readonly ctimeMs: number;
39
+ readonly mode: number;
40
+ readonly ino = 0;
41
+ readonly uid = 0;
42
+ readonly gid = 0;
43
+ readonly dev = 0;
44
+
45
+ constructor(private readonly info: WorkspaceFileInfo) {
46
+ this.size = info.size;
47
+ this.mtime = new Date(info.updatedAt);
48
+ this.mtimeMs = info.updatedAt;
49
+ this.ctimeMs = info.createdAt;
50
+ this.mode = info.type === "directory"
51
+ ? 0o40755
52
+ : info.type === "symlink"
53
+ ? 0o120000
54
+ : 0o100644;
55
+ }
56
+
57
+ isFile() {
58
+ return this.info.type === "file";
59
+ }
60
+
61
+ isDirectory() {
62
+ return this.info.type === "directory";
63
+ }
64
+
65
+ isSymbolicLink() {
66
+ return this.info.type === "symlink";
67
+ }
68
+ }
69
+
70
+ function fsError(path: string, cause?: unknown): Error & { code: string } {
71
+ if (
72
+ cause instanceof Error &&
73
+ "code" in cause &&
74
+ typeof (cause as { code: unknown }).code === "string"
75
+ ) return cause as Error & { code: string };
76
+
77
+ const error = new Error(
78
+ cause instanceof Error ? cause.message : `ENOENT: ${path}`,
79
+ ) as Error & { code: string };
80
+ error.code = cause === undefined ? "ENOENT" : "EIO";
81
+ return error;
82
+ }
83
+
84
+ async function readAll(workspace: WorkspacePort, path: string) {
85
+ const entries: WorkspaceFileInfo[] = [];
86
+ let offset = 0;
87
+ while (true) {
88
+ const page = await workspace.readDir(path, { limit: PAGE_SIZE, offset });
89
+ if (page.length === 0) return entries;
90
+ entries.push(...page);
91
+ offset += page.length;
92
+ }
93
+ }
94
+
95
+ export function createWorkspaceGitFs(workspace: WorkspacePort): FsClient {
96
+ const promises: PromiseFsClient["promises"] = {
97
+ async readFile(
98
+ path: string,
99
+ options?: { encoding?: string } | string,
100
+ ): Promise<Uint8Array | string> {
101
+ const encoding = typeof options === "string" ? options : options?.encoding;
102
+ try {
103
+ const value = encoding === "utf8" || encoding === "utf-8"
104
+ ? await workspace.readFile(path)
105
+ : await workspace.readFileBytes(path);
106
+ if (value === null) throw fsError(path);
107
+ return value;
108
+ } catch (error) {
109
+ throw fsError(path, error);
110
+ }
111
+ },
112
+ async writeFile(path: string, data: string | Uint8Array): Promise<void> {
113
+ const parent = path.replace(/\/[^/]+$/u, "");
114
+ if (parent && parent !== "/" && parent !== path) {
115
+ await workspace.mkdir(parent, { recursive: true });
116
+ }
117
+ if (typeof data === "string") await workspace.writeFile(path, data);
118
+ else await workspace.writeFileBytes(path, data);
119
+ },
120
+ async unlink(path: string): Promise<void> {
121
+ try {
122
+ await workspace.rm(path);
123
+ } catch (error) {
124
+ throw fsError(path, error);
125
+ }
126
+ },
127
+ async readdir(path: string): Promise<string[]> {
128
+ try {
129
+ const info = await workspace.stat(path);
130
+ if (!info || info.type !== "directory") throw fsError(path);
131
+ return (await readAll(workspace, path)).map(({ name }) => name);
132
+ } catch (error) {
133
+ throw fsError(path, error);
134
+ }
135
+ },
136
+ async mkdir(
137
+ path: string,
138
+ mode?: number | { recursive?: boolean },
139
+ ): Promise<void> {
140
+ await workspace.mkdir(path, {
141
+ recursive: typeof mode === "object" ? mode.recursive : false,
142
+ });
143
+ },
144
+ async rmdir(path: string): Promise<void> {
145
+ await workspace.rm(path);
146
+ },
147
+ async stat(path: string): Promise<GitStat> {
148
+ try {
149
+ const info = await workspace.stat(path);
150
+ if (!info) throw fsError(path);
151
+ return new GitStat(info);
152
+ } catch (error) {
153
+ throw fsError(path, error);
154
+ }
155
+ },
156
+ async lstat(path: string): Promise<GitStat> {
157
+ try {
158
+ const info = await workspace.lstat(path);
159
+ if (!info) throw fsError(path);
160
+ return new GitStat(info);
161
+ } catch (error) {
162
+ throw fsError(path, error);
163
+ }
164
+ },
165
+ async readlink(path: string): Promise<string> {
166
+ try {
167
+ return await workspace.readlink(path);
168
+ } catch (error) {
169
+ throw fsError(path, error);
170
+ }
171
+ },
172
+ async symlink(target: string, path: string): Promise<void> {
173
+ await workspace.symlink(target, path);
174
+ },
175
+ async chmod(): Promise<void> {},
176
+ };
177
+ return { promises };
178
+ }