@codeworksh/harness 0.0.1-dev.20260925163524 → 0.1.0-canary.1.1.gc0ff5572
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/{daytona-BAurx6Gp.mjs → daytona-ByTHZelc.mjs} +24 -14
- package/daytona-ByTHZelc.mjs.map +1 -0
- package/effect.d.mts +2334 -841
- package/effect.mjs +3022 -741
- package/effect.mjs.map +1 -1
- package/{index-DIP2u0Pr.d.mts → index-BEZ8eL--.d.mts} +2 -1
- package/{index-DjPzwUHF.d.mts → index-BlvIgEtU.d.mts} +2 -1
- package/package.json +8 -6
- package/resize-Dwv5dCgY.mjs +263 -0
- package/resize-Dwv5dCgY.mjs.map +1 -0
- package/sandboxes/daytona/index.d.mts +1 -1
- package/sandboxes/daytona/index.mjs +1 -1
- package/sandboxes/vercel/index.d.mts +1 -1
- package/sandboxes/vercel/index.mjs +1 -1
- package/tool/image/worker.d.mts +1 -0
- package/tool/image/worker.mjs +19 -0
- package/tool/image/worker.mjs.map +1 -0
- package/{vercel-C0v5nh20.mjs → vercel-3GouQSJI.mjs} +32 -4
- package/vercel-3GouQSJI.mjs.map +1 -0
- package/daytona-BAurx6Gp.mjs.map +0 -1
- package/vercel-C0v5nh20.mjs.map +0 -1
package/effect.mjs
CHANGED
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
import { n as __reExport, t as __exportAll } from "./rolldown-runtime-B4FMCO8f.mjs";
|
|
2
2
|
import { a as io_exports, i as errors_exports, n as shell_exports, o as instance_exports, r as filesystem_exports, s as posix } from "./resource-DFZh0e-s.mjs";
|
|
3
|
-
import "./daytona-
|
|
4
|
-
import "./vercel-
|
|
3
|
+
import "./daytona-ByTHZelc.mjs";
|
|
4
|
+
import "./vercel-3GouQSJI.mjs";
|
|
5
|
+
import { a as decode, n as dimensionNote, o as load$4, r as resize } from "./resize-Dwv5dCgY.mjs";
|
|
5
6
|
import { createRequire, registerHooks } from "node:module";
|
|
6
|
-
import { Cause, Clock, Config, Context, Crypto, DateTime, Deferred, Duration, Effect,
|
|
7
|
+
import { Cause, Clock, Config, Context, Crypto, DateTime, Deferred, Duration, Effect, Exit, Fiber, FiberSet, FileSystem, Layer, LayerMap, Option, Path, Predicate, PubSub, Queue, Random, Ref, Result, Schedule, Schema, SchemaIssue, Scope, Semaphore, Stream, String as String$1 } from "effect";
|
|
8
|
+
import { Message, Model, getEnvApiKey, llm, normalizeOptionalNulls, stream } from "@codeworksh/aikit";
|
|
7
9
|
import { MessageID as ID, SessionID as ID$1 } from "@codeworksh/plugin/ids";
|
|
8
|
-
import { Migrator, SqlClient, SqlClient as SqlClient$1, SqlError, SqlSchema, SqlSchema as SqlSchema$1 } from "effect/
|
|
10
|
+
import { Migrator, SqlClient, SqlClient as SqlClient$1, SqlError, SqlSchema, SqlSchema as SqlSchema$1 } from "effect/sql";
|
|
9
11
|
import { uuidv7 } from "uuidv7";
|
|
10
12
|
import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node";
|
|
11
13
|
import * as os from "node:os";
|
|
12
|
-
import { homedir
|
|
14
|
+
import { homedir } from "node:os";
|
|
13
15
|
import { NodeChildProcessSpawner, NodeCrypto, NodeFileSystem, NodePath } from "@effect/platform-node";
|
|
14
|
-
import { Model, VariantSchema } from "effect/
|
|
15
|
-
import {
|
|
16
|
+
import { Model as Model$1, VariantSchema } from "effect/schema";
|
|
17
|
+
import { Hex } from "effect/encoding";
|
|
16
18
|
import { MemoryProvider, RealFSProvider, SqliteProvider, create } from "@platformatic/vfs";
|
|
17
19
|
import fs, { statSync } from "node:fs";
|
|
18
|
-
import { ChildProcess, ChildProcessSpawner } from "effect/
|
|
20
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
19
21
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
20
22
|
import { Bash } from "just-bash";
|
|
21
|
-
import { DirectoryNotFoundError, DirectoryNotFoundError as DirectoryNotFoundError$1, Info, Info as Info$1, NotDirectoryError, NotDirectoryError as NotDirectoryError$1,
|
|
23
|
+
import { DirectoryNotFoundError, DirectoryNotFoundError as DirectoryNotFoundError$1, Info, Info as Info$1, NotDirectoryError, NotDirectoryError as NotDirectoryError$1, Service, Service as Service$8 } from "@codeworksh/plugin/location";
|
|
22
24
|
import { generateModels } from "@codeworksh/aikit/modelgen";
|
|
25
|
+
import { JsonGitHubCopilotAuthStorage, getGitHubCopilotApiKey } from "@codeworksh/aikit/oauth/github/copilot";
|
|
26
|
+
import { JsonOpenAICodexAuthStorage, getOpenAICodexApiKey } from "@codeworksh/aikit/oauth/openai/codex";
|
|
27
|
+
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
28
|
+
import * as Section from "@codeworksh/plugin/plugin/section";
|
|
29
|
+
import * as Diff from "diff";
|
|
30
|
+
import { FetchHttpClient, HttpClient } from "effect/http";
|
|
23
31
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
24
32
|
import path from "node:path";
|
|
25
33
|
import { Script, constants } from "node:vm";
|
|
26
34
|
import npa from "npm-package-arg";
|
|
27
35
|
import * as AikitFailure from "@codeworksh/aikit/failure";
|
|
28
|
-
import { JsonGitHubCopilotAuthStorage, getGitHubCopilotApiKey } from "@codeworksh/aikit/oauth/github/copilot";
|
|
29
|
-
import { JsonOpenAICodexAuthStorage, getOpenAICodexApiKey } from "@codeworksh/aikit/oauth/openai/codex";
|
|
30
36
|
import "@codeworksh/plugin/tool/error";
|
|
31
|
-
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
32
37
|
//#region src/schema.ts
|
|
33
38
|
var schema_exports$9 = /* @__PURE__ */ __exportAll({});
|
|
34
39
|
import * as import__codeworksh_plugin_schema from "@codeworksh/plugin/schema";
|
|
@@ -38,15 +43,46 @@ __reExport(schema_exports$9, import__codeworksh_plugin_schema);
|
|
|
38
43
|
var schema_exports$8 = /* @__PURE__ */ __exportAll({
|
|
39
44
|
AssistantEnvelopeUsage: () => AssistantEnvelopeUsage,
|
|
40
45
|
CompactionData: () => CompactionData,
|
|
46
|
+
Config: () => Config$1,
|
|
47
|
+
DEFAULT_TITLE: () => DEFAULT_TITLE,
|
|
41
48
|
ID: () => ID$1,
|
|
42
49
|
IDFromDb: () => IDFromDb,
|
|
43
50
|
InterruptReason: () => InterruptReason,
|
|
44
51
|
JsonObject: () => JsonObject,
|
|
45
52
|
MessageEnvelopeIdentity: () => MessageEnvelopeIdentity,
|
|
46
53
|
SessionSchema: () => schema_exports$8,
|
|
47
|
-
|
|
54
|
+
TITLE_MAX_LENGTH: () => 48,
|
|
55
|
+
ThinkingLevel: () => ThinkingLevel,
|
|
56
|
+
Usage: () => Usage,
|
|
57
|
+
titleFrom: () => titleFrom
|
|
48
58
|
});
|
|
49
59
|
const IDFromDb = Schema.String.pipe(Schema.brand("Session.ID"));
|
|
60
|
+
/** The title of a session created without one; its first prompt replaces it. */
|
|
61
|
+
const DEFAULT_TITLE = "Session";
|
|
62
|
+
/**
|
|
63
|
+
* A title from a prompt: its first non-empty line, whitespace collapsed, cut on a word boundary
|
|
64
|
+
* to {@link TITLE_MAX_LENGTH} characters including the ellipsis. `undefined` for a blank prompt.
|
|
65
|
+
*/
|
|
66
|
+
const titleFrom = (text) => {
|
|
67
|
+
const line = text.split("\n").map((one) => one.replace(/\s+/g, " ").trim()).find((one) => one.length > 0);
|
|
68
|
+
if (line === void 0 || line.length <= 48) return line;
|
|
69
|
+
const cut = line.slice(0, 47);
|
|
70
|
+
const space = cut.lastIndexOf(" ");
|
|
71
|
+
return `${(space > 0 ? cut.slice(0, space) : cut).trimEnd()}…`;
|
|
72
|
+
};
|
|
73
|
+
const ThinkingLevel = Schema.Literals(Object.values(Model.ThinkingLevelEnum));
|
|
74
|
+
/**
|
|
75
|
+
* The model and thinking level chosen for a session, over its settings. Every key is optional:
|
|
76
|
+
* an absent one means the session follows its settings. `session.config.changed` carries only the
|
|
77
|
+
* keys it changes, and the `session.config` column holds them merged.
|
|
78
|
+
*/
|
|
79
|
+
const Config$1 = Schema.Struct({
|
|
80
|
+
model: (0, schema_exports$9.optional)(Schema.Struct({
|
|
81
|
+
provider: Schema.String,
|
|
82
|
+
id: Schema.String
|
|
83
|
+
})),
|
|
84
|
+
thinkingLevel: (0, schema_exports$9.optional)(ThinkingLevel)
|
|
85
|
+
});
|
|
50
86
|
/**
|
|
51
87
|
* Why an execution stopped. Supplied by whoever asked for the interruption --
|
|
52
88
|
* a stop the user asked for and a stop the process imposed on shutdown are
|
|
@@ -101,12 +137,12 @@ var global_exports = /* @__PURE__ */ __exportAll({
|
|
|
101
137
|
app: () => app,
|
|
102
138
|
appConfigDir: () => appConfigDir,
|
|
103
139
|
authFile: () => authFile,
|
|
104
|
-
defaultLayer: () => defaultLayer$
|
|
140
|
+
defaultLayer: () => defaultLayer$2,
|
|
105
141
|
homeConfig: () => homeConfig,
|
|
106
142
|
layer: () => layer$27,
|
|
107
|
-
layerWith: () => layerWith
|
|
143
|
+
layerWith: () => layerWith,
|
|
108
144
|
make: () => make$18,
|
|
109
|
-
resolve: () => resolve$
|
|
145
|
+
resolve: () => resolve$4
|
|
110
146
|
});
|
|
111
147
|
const appConfigDir = ".codework";
|
|
112
148
|
const app = "codework";
|
|
@@ -120,7 +156,8 @@ function make$18(input = {}) {
|
|
|
120
156
|
home,
|
|
121
157
|
cache: input.cache ?? posix.join(home, "cache"),
|
|
122
158
|
data: input.data ?? posix.join(home, "data"),
|
|
123
|
-
log: input.log ?? posix.join(home, "log")
|
|
159
|
+
log: input.log ?? posix.join(home, "log"),
|
|
160
|
+
bin: input.bin ?? posix.join(home, "bin")
|
|
124
161
|
};
|
|
125
162
|
}
|
|
126
163
|
/**
|
|
@@ -133,7 +170,7 @@ function make$18(input = {}) {
|
|
|
133
170
|
* would be invisible to the other.
|
|
134
171
|
*/
|
|
135
172
|
const authFile = (home) => home === void 0 ? void 0 : posix.join(home, "aikit", "auth.json");
|
|
136
|
-
const resolve$
|
|
173
|
+
const resolve$4 = Effect.fn("Global.resolve")(function* (input = {}) {
|
|
137
174
|
const home = input.home === void 0 ? yield* homeConfig : expandHome(input.home);
|
|
138
175
|
return make$18({
|
|
139
176
|
...input,
|
|
@@ -141,7 +178,7 @@ const resolve$3 = Effect.fn("Global.resolve")(function* (input = {}) {
|
|
|
141
178
|
});
|
|
142
179
|
});
|
|
143
180
|
const build = (input) => Effect.gen(function* () {
|
|
144
|
-
const paths = yield* resolve$
|
|
181
|
+
const paths = yield* resolve$4(input);
|
|
145
182
|
yield* Effect.all([
|
|
146
183
|
fileSystem.makeDirectory(paths.cache, { recursive: true }),
|
|
147
184
|
fileSystem.makeDirectory(paths.data, { recursive: true }),
|
|
@@ -150,8 +187,8 @@ const build = (input) => Effect.gen(function* () {
|
|
|
150
187
|
return Service$17.of(paths);
|
|
151
188
|
});
|
|
152
189
|
const layer$27 = Layer.effect(Service$17, build({}));
|
|
153
|
-
const defaultLayer$
|
|
154
|
-
const layerWith
|
|
190
|
+
const defaultLayer$2 = layer$27;
|
|
191
|
+
const layerWith = (input) => Layer.effect(Service$17, build(input));
|
|
155
192
|
//#endregion
|
|
156
193
|
//#region src/db/migrations.ts
|
|
157
194
|
const migrations = {
|
|
@@ -329,6 +366,9 @@ const migrations = {
|
|
|
329
366
|
}),
|
|
330
367
|
"202609190001_session_host_dir": Effect.gen(function* () {
|
|
331
368
|
yield* (yield* SqlClient.SqlClient)`ALTER TABLE session ADD COLUMN host_dir TEXT`;
|
|
369
|
+
}),
|
|
370
|
+
"202609290001_session_config": Effect.gen(function* () {
|
|
371
|
+
yield* (yield* SqlClient.SqlClient)`ALTER TABLE session ADD COLUMN config TEXT`;
|
|
332
372
|
})
|
|
333
373
|
};
|
|
334
374
|
//#endregion
|
|
@@ -337,10 +377,10 @@ var db_exports = /* @__PURE__ */ __exportAll({
|
|
|
337
377
|
Database: () => db_exports,
|
|
338
378
|
SqlClient: () => SqlClient$1,
|
|
339
379
|
SqlSchema: () => SqlSchema$1,
|
|
340
|
-
defaultLayer: () => defaultLayer$
|
|
380
|
+
defaultLayer: () => defaultLayer$1,
|
|
341
381
|
layer: () => layer$26,
|
|
342
382
|
locationConfig: () => locationConfig,
|
|
343
|
-
path: () => path$
|
|
383
|
+
path: () => path$2,
|
|
344
384
|
resolveDatabaseLocation: () => resolveDatabaseLocation
|
|
345
385
|
});
|
|
346
386
|
const setup = Effect.gen(function* () {
|
|
@@ -367,11 +407,11 @@ function resolveDatabaseLocation(configured, data) {
|
|
|
367
407
|
if (configured === ":memory:" || posix.isAbsolute(configured)) return configured;
|
|
368
408
|
return posix.join(data, configured);
|
|
369
409
|
}
|
|
370
|
-
const path$
|
|
371
|
-
const directory = data ?? (yield* resolve$
|
|
410
|
+
const path$2 = Effect.fn("Database.path")(function* (data) {
|
|
411
|
+
const directory = data ?? (yield* resolve$4()).data;
|
|
372
412
|
return resolveDatabaseLocation(yield* locationConfig, directory);
|
|
373
413
|
});
|
|
374
|
-
const defaultLayer$
|
|
414
|
+
const defaultLayer$1 = Layer.unwrap(path$2().pipe(Effect.map(layer$26)));
|
|
375
415
|
//#endregion
|
|
376
416
|
//#region src/event/schema.ts
|
|
377
417
|
var schema_exports$7 = /* @__PURE__ */ __exportAll({});
|
|
@@ -386,13 +426,54 @@ __reExport(schema_exports$6, import__codeworksh_plugin_project);
|
|
|
386
426
|
//#region src/session/prompt/schema.ts
|
|
387
427
|
var schema_exports = /* @__PURE__ */ __exportAll({
|
|
388
428
|
Delivery: () => Delivery,
|
|
429
|
+
IMAGE_MAX_BASE64: () => IMAGE_MAX_BASE64,
|
|
430
|
+
IMAGE_TYPES: () => IMAGE_TYPES,
|
|
431
|
+
ImagePart: () => ImagePart,
|
|
432
|
+
InvalidPromptError: () => InvalidPromptError,
|
|
433
|
+
Part: () => Part,
|
|
389
434
|
Prompt: () => Prompt,
|
|
390
|
-
PromptSchema: () => schema_exports
|
|
435
|
+
PromptSchema: () => schema_exports,
|
|
436
|
+
TextPart: () => TextPart,
|
|
437
|
+
imageProblem: () => imageProblem
|
|
438
|
+
});
|
|
439
|
+
const IMAGE_TYPES = [
|
|
440
|
+
"image/png",
|
|
441
|
+
"image/jpeg",
|
|
442
|
+
"image/gif",
|
|
443
|
+
"image/webp"
|
|
444
|
+
];
|
|
445
|
+
const IMAGE_MAX_BASE64 = 4718592;
|
|
446
|
+
const BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
447
|
+
const imageProblem = (image) => {
|
|
448
|
+
if (!IMAGE_TYPES.includes(image.mimeType)) return `Unsupported image type ${image.mimeType}; use ${IMAGE_TYPES.join(", ")}`;
|
|
449
|
+
if (image.data.length > 4718592) return `Image exceeds the ${IMAGE_MAX_BASE64 / 1024 / 1024} MB limit of base64`;
|
|
450
|
+
if (image.data.length === 0 || image.data.length % 4 !== 0 || !BASE64.test(image.data)) return "Image data is not base64";
|
|
451
|
+
};
|
|
452
|
+
const TextPart = Schema.Struct({
|
|
453
|
+
type: Schema.Literal("text"),
|
|
454
|
+
text: Schema.String
|
|
391
455
|
});
|
|
392
|
-
|
|
456
|
+
/** aikit's image content: base64 `data` and its `mimeType`. */
|
|
457
|
+
const ImagePart = Schema.Struct({
|
|
458
|
+
type: Schema.Literal("image"),
|
|
459
|
+
data: Schema.String,
|
|
460
|
+
mimeType: Schema.String
|
|
461
|
+
}).check(Schema.makeFilter(imageProblem));
|
|
462
|
+
const Part = Schema.Union([TextPart, ImagePart]);
|
|
463
|
+
/** A user prompt: aikit user-message parts, in the order they were written. */
|
|
464
|
+
const Prompt = Schema.Struct({ parts: Schema.Array(Part).check(Schema.isMinLength(1)) }).annotate({ identifier: "Prompt" }).pipe((0, schema_exports$9.withStatics)((schema) => ({
|
|
393
465
|
equivalence: Schema.toEquivalence(schema),
|
|
394
|
-
|
|
466
|
+
fromText: (text) => schema.make({ parts: [{
|
|
467
|
+
type: "text",
|
|
468
|
+
text
|
|
469
|
+
}] }),
|
|
470
|
+
text: (prompt) => prompt.parts.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n\n")
|
|
395
471
|
})));
|
|
472
|
+
var InvalidPromptError = class extends Schema.TaggedError()("InvalidPromptError", { reason: Schema.String }) {
|
|
473
|
+
get message() {
|
|
474
|
+
return this.reason;
|
|
475
|
+
}
|
|
476
|
+
};
|
|
396
477
|
const Delivery = Schema.Literals(["steer", "followUp"]);
|
|
397
478
|
//#endregion
|
|
398
479
|
//#region src/space/schema.ts
|
|
@@ -402,8 +483,8 @@ __reExport(schema_exports$5, import__codeworksh_plugin_space);
|
|
|
402
483
|
//#endregion
|
|
403
484
|
//#region src/db/schema.sql.ts
|
|
404
485
|
const Timestamps = {
|
|
405
|
-
createdAt: Model.DateTimeInsertFromNumber,
|
|
406
|
-
updatedAt: Model.DateTimeUpdateFromNumber
|
|
486
|
+
createdAt: Model$1.DateTimeInsertFromNumber,
|
|
487
|
+
updatedAt: Model$1.DateTimeUpdateFromNumber
|
|
407
488
|
};
|
|
408
489
|
const Metadata = Schema.Record(Schema.String, Schema.String);
|
|
409
490
|
const entryTypes = [
|
|
@@ -441,19 +522,19 @@ const toolStatuses = [
|
|
|
441
522
|
];
|
|
442
523
|
const inputDeliveries = ["steer", "followUp"];
|
|
443
524
|
const EventData = Schema.Record(Schema.String, Schema.Unknown);
|
|
444
|
-
Model.Class("EventSequenceRow")({
|
|
525
|
+
Model$1.Class("EventSequenceRow")({
|
|
445
526
|
aggregateId: Schema.String,
|
|
446
527
|
seq: Schema.Natural,
|
|
447
|
-
ownerId: Model.FieldOption(Schema.String)
|
|
528
|
+
ownerId: Model$1.FieldOption(Schema.String)
|
|
448
529
|
});
|
|
449
|
-
var EventRow = class extends Model.Class("EventRow")({
|
|
530
|
+
var EventRow = class extends Model$1.Class("EventRow")({
|
|
450
531
|
id: schema_exports$7.EventSchema.ID,
|
|
451
532
|
aggregateId: Schema.String,
|
|
452
533
|
seq: Schema.Natural,
|
|
453
534
|
type: Schema.String,
|
|
454
|
-
data: Model.JsonFromString(EventData)
|
|
535
|
+
data: Model$1.JsonFromString(EventData)
|
|
455
536
|
}) {};
|
|
456
|
-
var ProjectRow = class extends Model.Class("ProjectRow")({
|
|
537
|
+
var ProjectRow = class extends Model$1.Class("ProjectRow")({
|
|
457
538
|
id: Schema.String,
|
|
458
539
|
name: Schema.String,
|
|
459
540
|
status: schema_exports$6.ProjectSchema.Status,
|
|
@@ -467,81 +548,82 @@ var ProjectRow = class extends Model.Class("ProjectRow")({
|
|
|
467
548
|
* The host has no row: `local` is reserved and `NULL` is its storage form, so
|
|
468
549
|
* this table only ever holds namespaces something had to provision.
|
|
469
550
|
*/
|
|
470
|
-
var SandboxInstanceRow = class extends Model.Class("SandboxInstanceRow")({
|
|
551
|
+
var SandboxInstanceRow = class extends Model$1.Class("SandboxInstanceRow")({
|
|
471
552
|
id: instance_exports.SandboxInstance.ID,
|
|
472
553
|
driver: Schema.String,
|
|
473
554
|
kind: instance_exports.SandboxInstance.Kind,
|
|
474
|
-
providerResourceId: Model.FieldOption(Schema.String),
|
|
475
|
-
runtimeConfig: Model.FieldOption(Schema.String),
|
|
555
|
+
providerResourceId: Model$1.FieldOption(Schema.String),
|
|
556
|
+
runtimeConfig: Model$1.FieldOption(Schema.String),
|
|
476
557
|
ownership: instance_exports.SandboxInstance.Ownership,
|
|
477
558
|
status: instance_exports.SandboxInstance.Status,
|
|
478
|
-
providerStatus: Model.FieldOption(Schema.String),
|
|
479
|
-
stateObservedAt: Model.DateTimeFromNumberWithNow,
|
|
480
|
-
metadata: Model.FieldOption(Model.JsonFromString(Metadata)),
|
|
481
|
-
lastError: Model.FieldOption(Model.JsonFromString(instance_exports.SandboxInstance.PersistedError)),
|
|
482
|
-
lastMountedAt: Model.FieldOption(Schema.DateTimeUtcFromMillis),
|
|
483
|
-
lastUnmountedAt: Model.FieldOption(Schema.DateTimeUtcFromMillis),
|
|
484
|
-
lastUsedAt: Model.FieldOption(Schema.DateTimeUtcFromMillis),
|
|
485
|
-
removedAt: Model.FieldOption(Schema.DateTimeUtcFromMillis),
|
|
559
|
+
providerStatus: Model$1.FieldOption(Schema.String),
|
|
560
|
+
stateObservedAt: Model$1.DateTimeFromNumberWithNow,
|
|
561
|
+
metadata: Model$1.FieldOption(Model$1.JsonFromString(Metadata)),
|
|
562
|
+
lastError: Model$1.FieldOption(Model$1.JsonFromString(instance_exports.SandboxInstance.PersistedError)),
|
|
563
|
+
lastMountedAt: Model$1.FieldOption(Schema.DateTimeUtcFromMillis),
|
|
564
|
+
lastUnmountedAt: Model$1.FieldOption(Schema.DateTimeUtcFromMillis),
|
|
565
|
+
lastUsedAt: Model$1.FieldOption(Schema.DateTimeUtcFromMillis),
|
|
566
|
+
removedAt: Model$1.FieldOption(Schema.DateTimeUtcFromMillis),
|
|
486
567
|
...Timestamps
|
|
487
568
|
}) {};
|
|
488
|
-
var SpaceRow = class extends Model.Class("SpaceRow")({
|
|
569
|
+
var SpaceRow = class extends Model$1.Class("SpaceRow")({
|
|
489
570
|
id: Schema.String,
|
|
490
571
|
projectId: Schema.String,
|
|
491
572
|
location: schema_exports$9.AbsolutePath,
|
|
492
573
|
kind: schema_exports$5.SpaceSchema.Kind,
|
|
493
|
-
env: Model.FieldOption(instance_exports.SandboxInstance.ID),
|
|
574
|
+
env: Model$1.FieldOption(instance_exports.SandboxInstance.ID),
|
|
494
575
|
status: schema_exports$5.SpaceSchema.Status,
|
|
495
576
|
...Timestamps
|
|
496
577
|
}) {};
|
|
497
|
-
var SessionRow = class extends Model.Class("SessionRow")({
|
|
578
|
+
var SessionRow = class extends Model$1.Class("SessionRow")({
|
|
498
579
|
id: IDFromDb,
|
|
499
580
|
spaceId: Schema.String,
|
|
500
|
-
parentId: Model.FieldOption(IDFromDb),
|
|
581
|
+
parentId: Model$1.FieldOption(IDFromDb),
|
|
501
582
|
slug: Schema.String,
|
|
502
583
|
directory: schema_exports$9.AbsolutePath,
|
|
503
|
-
hostDir: Model.FieldOption(schema_exports$9.AbsolutePath),
|
|
584
|
+
hostDir: Model$1.FieldOption(schema_exports$9.AbsolutePath),
|
|
585
|
+
config: Model$1.FieldOption(Model$1.JsonFromString(Config$1)),
|
|
504
586
|
title: Schema.String,
|
|
505
|
-
tag: Model.FieldOption(Schema.String),
|
|
506
|
-
metadata: Model.FieldOption(Model.JsonFromString(Metadata)),
|
|
507
|
-
leafEntryId: Model.FieldOption(Schema.String),
|
|
508
|
-
cost: Model.GeneratedByDb(Schema.Finite),
|
|
509
|
-
tokensInput: Model.GeneratedByDb(Schema.Int),
|
|
510
|
-
tokensOutput: Model.GeneratedByDb(Schema.Int),
|
|
511
|
-
tokensCacheRead: Model.GeneratedByDb(Schema.Int),
|
|
512
|
-
tokensCacheWrite: Model.GeneratedByDb(Schema.Int),
|
|
587
|
+
tag: Model$1.FieldOption(Schema.String),
|
|
588
|
+
metadata: Model$1.FieldOption(Model$1.JsonFromString(Metadata)),
|
|
589
|
+
leafEntryId: Model$1.FieldOption(Schema.String),
|
|
590
|
+
cost: Model$1.GeneratedByDb(Schema.Finite),
|
|
591
|
+
tokensInput: Model$1.GeneratedByDb(Schema.Int),
|
|
592
|
+
tokensOutput: Model$1.GeneratedByDb(Schema.Int),
|
|
593
|
+
tokensCacheRead: Model$1.GeneratedByDb(Schema.Int),
|
|
594
|
+
tokensCacheWrite: Model$1.GeneratedByDb(Schema.Int),
|
|
513
595
|
...Timestamps
|
|
514
596
|
}) {};
|
|
515
|
-
var SessionInputRow = class extends Model.Class("SessionInputRow")({
|
|
597
|
+
var SessionInputRow = class extends Model$1.Class("SessionInputRow")({
|
|
516
598
|
id: Schema.String,
|
|
517
599
|
sessionId: IDFromDb,
|
|
518
|
-
prompt: Model.JsonFromString(Prompt),
|
|
600
|
+
prompt: Model$1.JsonFromString(Prompt),
|
|
519
601
|
delivery: Schema.Literals(inputDeliveries),
|
|
520
602
|
admittedSeq: Schema.Int,
|
|
521
|
-
promotedSeq: Model.FieldOption(Schema.Int),
|
|
522
|
-
createdAt: Model.DateTimeInsertFromNumber
|
|
603
|
+
promotedSeq: Model$1.FieldOption(Schema.Int),
|
|
604
|
+
createdAt: Model$1.DateTimeInsertFromNumber
|
|
523
605
|
}) {};
|
|
524
|
-
var SessionEntryRow = class extends Model.Class("SessionEntryRow")({
|
|
606
|
+
var SessionEntryRow = class extends Model$1.Class("SessionEntryRow")({
|
|
525
607
|
id: Schema.String,
|
|
526
608
|
sessionId: IDFromDb,
|
|
527
|
-
parentId: Model.FieldOption(Schema.String),
|
|
609
|
+
parentId: Model$1.FieldOption(Schema.String),
|
|
528
610
|
seq: schema_exports$9.NonNegativeInt,
|
|
529
611
|
type: Schema.Literals(entryTypes),
|
|
530
612
|
state: Schema.Literals(entryStates),
|
|
531
613
|
data: Schema.String,
|
|
532
|
-
label: Model.FieldOption(Schema.String),
|
|
533
|
-
metadata: Model.FieldOption(Model.JsonFromString(Metadata)),
|
|
614
|
+
label: Model$1.FieldOption(Schema.String),
|
|
615
|
+
metadata: Model$1.FieldOption(Model$1.JsonFromString(Metadata)),
|
|
534
616
|
...Timestamps
|
|
535
617
|
}) {};
|
|
536
|
-
var SessionEntryPartRow = class extends Model.Class("SessionEntryPartRow")({
|
|
618
|
+
var SessionEntryPartRow = class extends Model$1.Class("SessionEntryPartRow")({
|
|
537
619
|
id: Schema.String,
|
|
538
620
|
entryId: Schema.String,
|
|
539
621
|
sessionId: IDFromDb,
|
|
540
622
|
partIndex: Schema.Int,
|
|
541
623
|
type: Schema.Literals(partTypes),
|
|
542
|
-
status: Model.FieldOption(Schema.Literals(toolStatuses)),
|
|
543
|
-
callId: Model.FieldOption(Schema.String),
|
|
544
|
-
toolName: Model.FieldOption(Schema.String),
|
|
624
|
+
status: Model$1.FieldOption(Schema.Literals(toolStatuses)),
|
|
625
|
+
callId: Model$1.FieldOption(Schema.String),
|
|
626
|
+
toolName: Model$1.FieldOption(Schema.String),
|
|
545
627
|
data: Schema.String,
|
|
546
628
|
...Timestamps
|
|
547
629
|
}) {};
|
|
@@ -631,6 +713,7 @@ var schema_exports$4 = /* @__PURE__ */ __exportAll({
|
|
|
631
713
|
//#endregion
|
|
632
714
|
//#region src/event/list.ts
|
|
633
715
|
var list_exports = /* @__PURE__ */ __exportAll({
|
|
716
|
+
ConfigChanged: () => ConfigChanged,
|
|
634
717
|
Definitions: () => Definitions,
|
|
635
718
|
DurableDefinitions: () => DurableDefinitions,
|
|
636
719
|
EventList: () => list_exports,
|
|
@@ -651,6 +734,8 @@ var list_exports = /* @__PURE__ */ __exportAll({
|
|
|
651
734
|
PromptAdmitted: () => PromptAdmitted,
|
|
652
735
|
Prompted: () => Prompted,
|
|
653
736
|
PublicDefinitions: () => PublicDefinitions,
|
|
737
|
+
RetryFinished: () => RetryFinished,
|
|
738
|
+
RetryScheduled: () => RetryScheduled,
|
|
654
739
|
SessionForked: () => SessionForked,
|
|
655
740
|
ToolProgress: () => ToolProgress$2,
|
|
656
741
|
ToolSettled: () => ToolSettled,
|
|
@@ -684,6 +769,14 @@ const Prompted = schema_exports$7.EventSchema.define({
|
|
|
684
769
|
...durableOptions,
|
|
685
770
|
schema: PromptFields
|
|
686
771
|
});
|
|
772
|
+
const ConfigChanged = schema_exports$7.EventSchema.define({
|
|
773
|
+
type: "session.config.changed",
|
|
774
|
+
...durableOptions,
|
|
775
|
+
schema: {
|
|
776
|
+
...baseOptions,
|
|
777
|
+
...Config$1.fields
|
|
778
|
+
}
|
|
779
|
+
});
|
|
687
780
|
const LLMFields = {
|
|
688
781
|
...baseOptions,
|
|
689
782
|
messageId: ID
|
|
@@ -713,6 +806,30 @@ const TurnAborted = schema_exports$7.EventSchema.define({
|
|
|
713
806
|
cause: TurnAbortCause
|
|
714
807
|
}
|
|
715
808
|
});
|
|
809
|
+
/**
|
|
810
|
+
* A failed provider request will be repeated after `delayMs`. `attempt` counts retries from 1, so
|
|
811
|
+
* the request that failed was attempt `attempt - 1`; the failed draft is already settled.
|
|
812
|
+
*/
|
|
813
|
+
const RetryScheduled = schema_exports$7.EventSchema.define({
|
|
814
|
+
type: "session.retry.scheduled",
|
|
815
|
+
schema: {
|
|
816
|
+
...baseOptions,
|
|
817
|
+
attempt: schema_exports$9.NonNegativeInt,
|
|
818
|
+
maxRetries: schema_exports$9.NonNegativeInt,
|
|
819
|
+
delayMs: schema_exports$9.NonNegativeInt,
|
|
820
|
+
message: Schema.String
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
/** Once per turn that retried: whether a retry succeeded, after how many. */
|
|
824
|
+
const RetryFinished = schema_exports$7.EventSchema.define({
|
|
825
|
+
type: "session.retry.finished",
|
|
826
|
+
schema: {
|
|
827
|
+
...baseOptions,
|
|
828
|
+
attempt: schema_exports$9.NonNegativeInt,
|
|
829
|
+
success: Schema.Boolean,
|
|
830
|
+
message: (0, schema_exports$9.optional)(Schema.String)
|
|
831
|
+
}
|
|
832
|
+
});
|
|
716
833
|
/** Durable insertion of the request's draft assistant placeholder. */
|
|
717
834
|
const LLMStarted = schema_exports$7.EventSchema.define({
|
|
718
835
|
type: "session.llm.started",
|
|
@@ -787,7 +904,10 @@ const ToolStarted = schema_exports$7.EventSchema.define({
|
|
|
787
904
|
schema: {
|
|
788
905
|
...LLMFields,
|
|
789
906
|
callID: Schema.String,
|
|
790
|
-
name: Schema.String
|
|
907
|
+
name: Schema.String,
|
|
908
|
+
/** The tool's display label, when its definition declares one. */
|
|
909
|
+
label: (0, schema_exports$9.optional)(Schema.String),
|
|
910
|
+
arguments: Schema.Record(Schema.String, Schema.Json)
|
|
791
911
|
}
|
|
792
912
|
});
|
|
793
913
|
const ToolProgress$2 = schema_exports$7.EventSchema.define({
|
|
@@ -883,14 +1003,14 @@ const PluginUpdated = schema_exports$7.EventSchema.define({
|
|
|
883
1003
|
* Everything the kernel can publish. `PublicDefinitions` is the narrower set
|
|
884
1004
|
* that reaches clients -- adding an event here does not publish it.
|
|
885
1005
|
*/
|
|
886
|
-
const Definitions = schema_exports$7.EventSchema.inventory(ExecutionStarted, ExecutionSucceeded, ExecutionFailed, ExecutionInterrupted, PromptAdmitted, Prompted, SessionForked, TurnStarted, TurnEnded, TurnAborted, LLMStarted, LLMTextStart, LLMTextDelta, LLMTextEnd, LLMThinkingStart, LLMThinkingDelta, LLMThinkingEnd, LLMEnded, LLMFailed, ToolStarted, ToolProgress$2, ToolSettled, PluginUpdated);
|
|
1006
|
+
const Definitions = schema_exports$7.EventSchema.inventory(ExecutionStarted, ExecutionSucceeded, ExecutionFailed, ExecutionInterrupted, PromptAdmitted, Prompted, ConfigChanged, SessionForked, TurnStarted, TurnEnded, TurnAborted, RetryScheduled, RetryFinished, LLMStarted, LLMTextStart, LLMTextDelta, LLMTextEnd, LLMThinkingStart, LLMThinkingDelta, LLMThinkingEnd, LLMEnded, LLMFailed, ToolStarted, ToolProgress$2, ToolSettled, PluginUpdated);
|
|
887
1007
|
/**
|
|
888
1008
|
* The wire contract: what a client is allowed to receive. Identical to
|
|
889
1009
|
* `Definitions` today, and separate so that adding a kernel event is not by
|
|
890
1010
|
* itself a decision to publish it.
|
|
891
1011
|
*/
|
|
892
1012
|
const PublicDefinitions = Definitions;
|
|
893
|
-
const DurableDefinitions = schema_exports$7.EventSchema.inventory(PromptAdmitted, Prompted, SessionForked, TurnEnded, LLMStarted, LLMEnded, LLMFailed, ToolSettled);
|
|
1013
|
+
const DurableDefinitions = schema_exports$7.EventSchema.inventory(PromptAdmitted, Prompted, ConfigChanged, SessionForked, TurnEnded, LLMStarted, LLMEnded, LLMFailed, ToolSettled);
|
|
894
1014
|
const Manifest$2 = {
|
|
895
1015
|
definitions: schema_exports$7.EventSchema.durable([...DurableDefinitions]),
|
|
896
1016
|
schema: Schema.Union([...DurableDefinitions], { mode: "oneOf" }).pipe(Schema.toTaggedUnion("type"))
|
|
@@ -1235,7 +1355,7 @@ const layer$25 = Layer.effect(Service$16, Effect.gen(function* () {
|
|
|
1235
1355
|
const encoder = new TextEncoder();
|
|
1236
1356
|
function fast(input) {
|
|
1237
1357
|
const data = typeof input === "string" ? encoder.encode(input) : input;
|
|
1238
|
-
return
|
|
1358
|
+
return Hex.encode(Effect.runSync(crypto.digest("SHA-1", data)));
|
|
1239
1359
|
}
|
|
1240
1360
|
//#endregion
|
|
1241
1361
|
//#region src/space/space.ts
|
|
@@ -1377,6 +1497,77 @@ const layer$24 = Layer.effect(Service$15, Effect.gen(function* () {
|
|
|
1377
1497
|
});
|
|
1378
1498
|
}));
|
|
1379
1499
|
//#endregion
|
|
1500
|
+
//#region src/util/slug.ts
|
|
1501
|
+
const PREFIXES = [
|
|
1502
|
+
"binary",
|
|
1503
|
+
"protocol",
|
|
1504
|
+
"astromech",
|
|
1505
|
+
"thermal",
|
|
1506
|
+
"ionic",
|
|
1507
|
+
"encrypted",
|
|
1508
|
+
"holonet",
|
|
1509
|
+
"hyperspace",
|
|
1510
|
+
"magnetic",
|
|
1511
|
+
"logic",
|
|
1512
|
+
"plasma",
|
|
1513
|
+
"vector",
|
|
1514
|
+
"optical",
|
|
1515
|
+
"quantum",
|
|
1516
|
+
"mechanical",
|
|
1517
|
+
"servo",
|
|
1518
|
+
"fusion",
|
|
1519
|
+
"flux",
|
|
1520
|
+
"sychro",
|
|
1521
|
+
"static"
|
|
1522
|
+
];
|
|
1523
|
+
const HARDWARE = [
|
|
1524
|
+
"core",
|
|
1525
|
+
"unit",
|
|
1526
|
+
"node",
|
|
1527
|
+
"array",
|
|
1528
|
+
"link",
|
|
1529
|
+
"processor",
|
|
1530
|
+
"module",
|
|
1531
|
+
"uplink",
|
|
1532
|
+
"interface",
|
|
1533
|
+
"splicer",
|
|
1534
|
+
"matrix",
|
|
1535
|
+
"buffer",
|
|
1536
|
+
"relay",
|
|
1537
|
+
"circuit",
|
|
1538
|
+
"sensor",
|
|
1539
|
+
"driver",
|
|
1540
|
+
"bridge",
|
|
1541
|
+
"manifold",
|
|
1542
|
+
"oscillator",
|
|
1543
|
+
"terminal"
|
|
1544
|
+
];
|
|
1545
|
+
const SECTORS = [
|
|
1546
|
+
"kuat",
|
|
1547
|
+
"corellia",
|
|
1548
|
+
"coruscant",
|
|
1549
|
+
"bespin",
|
|
1550
|
+
"kamino",
|
|
1551
|
+
"mustafar",
|
|
1552
|
+
"fondor",
|
|
1553
|
+
"hosnian",
|
|
1554
|
+
"lothal",
|
|
1555
|
+
"scarif",
|
|
1556
|
+
"sub-level",
|
|
1557
|
+
"deep-space",
|
|
1558
|
+
"outer-rim",
|
|
1559
|
+
"mid-rim",
|
|
1560
|
+
"sector-7"
|
|
1561
|
+
];
|
|
1562
|
+
const CONNECTORS = [
|
|
1563
|
+
"at",
|
|
1564
|
+
"of",
|
|
1565
|
+
"in"
|
|
1566
|
+
];
|
|
1567
|
+
const create$3 = Effect.fnUntraced(function* (id) {
|
|
1568
|
+
return `${yield* Random.choice(PREFIXES)}-${yield* Random.choice(HARDWARE)}-${yield* Random.choice(CONNECTORS)}-${yield* Random.choice(SECTORS)}-${id.slice(-8)}`;
|
|
1569
|
+
});
|
|
1570
|
+
//#endregion
|
|
1380
1571
|
//#region src/session/session.ts
|
|
1381
1572
|
var session_exports$1 = /* @__PURE__ */ __exportAll({
|
|
1382
1573
|
EntryNotFoundError: () => EntryNotFoundError,
|
|
@@ -1391,7 +1582,7 @@ var session_exports$1 = /* @__PURE__ */ __exportAll({
|
|
|
1391
1582
|
SessionNotLinkedError: () => SessionNotLinkedError,
|
|
1392
1583
|
SessionRow: () => SessionRow,
|
|
1393
1584
|
ToolCallNotFoundError: () => ToolCallNotFoundError,
|
|
1394
|
-
defaultLayer: () => defaultLayer
|
|
1585
|
+
defaultLayer: () => defaultLayer,
|
|
1395
1586
|
entryStates: () => entryStates,
|
|
1396
1587
|
entryTypes: () => entryTypes,
|
|
1397
1588
|
layer: () => layer$23,
|
|
@@ -1603,9 +1794,10 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
|
|
|
1603
1794
|
id,
|
|
1604
1795
|
spaceId: input.spaceId,
|
|
1605
1796
|
parentId: Option.fromUndefinedOr(input.parentId),
|
|
1606
|
-
slug:
|
|
1797
|
+
slug: yield* create$3(id),
|
|
1607
1798
|
directory: input.directory,
|
|
1608
1799
|
hostDir: Option.fromUndefinedOr(input.hostDir),
|
|
1800
|
+
config: Option.none(),
|
|
1609
1801
|
title: input.title,
|
|
1610
1802
|
tag: Option.fromUndefinedOr(input.tag),
|
|
1611
1803
|
metadata: Option.fromUndefinedOr(input.metadata),
|
|
@@ -1948,9 +2140,10 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
|
|
|
1948
2140
|
id: newSessionId,
|
|
1949
2141
|
spaceId: source.value.spaceId,
|
|
1950
2142
|
parentId: Option.some(source.value.id),
|
|
1951
|
-
slug:
|
|
2143
|
+
slug: yield* create$3(newSessionId),
|
|
1952
2144
|
directory: source.value.directory,
|
|
1953
2145
|
hostDir: source.value.hostDir,
|
|
2146
|
+
config: source.value.config,
|
|
1954
2147
|
title: input.title ?? source.value.title,
|
|
1955
2148
|
tag: input.tag === void 0 ? source.value.tag : Option.some(input.tag),
|
|
1956
2149
|
metadata: source.value.metadata,
|
|
@@ -2045,6 +2238,21 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
|
|
|
2045
2238
|
if (Option.isNone(updated)) return yield* Effect.die(/* @__PURE__ */ new Error("session link did not persist"));
|
|
2046
2239
|
return updated.value;
|
|
2047
2240
|
});
|
|
2241
|
+
const retitle = Effect.fn("Session.retitle")(function* (sessionId, title) {
|
|
2242
|
+
return (yield* sql`
|
|
2243
|
+
UPDATE session SET title = ${title}
|
|
2244
|
+
WHERE id = ${sessionId} AND title = ${DEFAULT_TITLE}
|
|
2245
|
+
RETURNING id
|
|
2246
|
+
`.pipe(Effect.orDie)).length > 0;
|
|
2247
|
+
});
|
|
2248
|
+
const encodeConfig = Schema.encodeEffect(Schema.fromJsonString(Config$1));
|
|
2249
|
+
const configure = Effect.fn("Session.configure")(function* (sessionId, config) {
|
|
2250
|
+
const patch = yield* encodeConfig(config);
|
|
2251
|
+
yield* sql`
|
|
2252
|
+
UPDATE session SET config = json_patch(COALESCE(config, '{}'), ${patch})
|
|
2253
|
+
WHERE id = ${sessionId}
|
|
2254
|
+
`;
|
|
2255
|
+
}, Effect.orDie);
|
|
2048
2256
|
const relink = Effect.fn("Session.relink")(function* (input) {
|
|
2049
2257
|
const reject = (reason) => new RelinkError({
|
|
2050
2258
|
sessionId: input.sessionId,
|
|
@@ -2104,6 +2312,8 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
|
|
|
2104
2312
|
return Service$14.of({
|
|
2105
2313
|
create,
|
|
2106
2314
|
link,
|
|
2315
|
+
retitle,
|
|
2316
|
+
configure,
|
|
2107
2317
|
get,
|
|
2108
2318
|
space,
|
|
2109
2319
|
list,
|
|
@@ -2122,7 +2332,7 @@ const layer$23 = Layer.effect(Service$14, Effect.gen(function* () {
|
|
|
2122
2332
|
setLabel
|
|
2123
2333
|
});
|
|
2124
2334
|
}));
|
|
2125
|
-
const defaultLayer
|
|
2335
|
+
const defaultLayer = layer$23.pipe(Layer.provide(defaultLayer$1));
|
|
2126
2336
|
//#endregion
|
|
2127
2337
|
//#region src/context/errors.ts
|
|
2128
2338
|
var ContextDecodeError = class extends Schema.TaggedError()("ContextDecodeError", {
|
|
@@ -2459,6 +2669,7 @@ const toFileStat = (stats) => ({
|
|
|
2459
2669
|
size: stats.size,
|
|
2460
2670
|
mtime: stats.mtime
|
|
2461
2671
|
});
|
|
2672
|
+
const SCAN_CHUNK = 65536;
|
|
2462
2673
|
const bytes = (content) => typeof content === "string" ? Buffer$1.from(content, "utf8") : Buffer$1.from(content);
|
|
2463
2674
|
/**
|
|
2464
2675
|
* Provider factory method that implements the SandboxFileSytem
|
|
@@ -2470,6 +2681,8 @@ const make$17 = (vfs) => {
|
|
|
2470
2681
|
await vfs.promises.writeFile(path, bytes(content));
|
|
2471
2682
|
};
|
|
2472
2683
|
const readdir = async (path) => vfs.promises.readdir(path);
|
|
2684
|
+
const open = (path) => new Promise((resolve, reject) => vfs.open(path, "r", (error, value) => error ? reject(error) : resolve(value)));
|
|
2685
|
+
const close = (fd) => new Promise((resolve) => vfs.close(fd, () => resolve()));
|
|
2473
2686
|
const rm = async (path, options) => {
|
|
2474
2687
|
let stats;
|
|
2475
2688
|
try {
|
|
@@ -2509,7 +2722,36 @@ const make$17 = (vfs) => {
|
|
|
2509
2722
|
await vfs.promises.mkdir(path, options);
|
|
2510
2723
|
},
|
|
2511
2724
|
rm,
|
|
2512
|
-
realpath: (path) => vfs.promises.realpath(path)
|
|
2725
|
+
realpath: (path) => vfs.promises.realpath(path),
|
|
2726
|
+
readBytes: async (path, offset, length) => {
|
|
2727
|
+
const fd = await open(path);
|
|
2728
|
+
try {
|
|
2729
|
+
const buffer = Buffer$1.alloc(length);
|
|
2730
|
+
const read = await new Promise((resolve, reject) => vfs.read(fd, buffer, 0, length, offset, (error, bytesRead) => error ? reject(error) : resolve(bytesRead)));
|
|
2731
|
+
return new Uint8Array(buffer.subarray(0, read));
|
|
2732
|
+
} finally {
|
|
2733
|
+
await close(fd);
|
|
2734
|
+
}
|
|
2735
|
+
},
|
|
2736
|
+
scanLines: async (path, options) => {
|
|
2737
|
+
const scanner = new filesystem_exports.SandboxFileSystem.Scan.LineScanner(options);
|
|
2738
|
+
const { size } = await vfs.promises.stat(path);
|
|
2739
|
+
if (size === 0) return scanner.finish();
|
|
2740
|
+
const fd = await open(path);
|
|
2741
|
+
try {
|
|
2742
|
+
const buffer = Buffer$1.alloc(Math.min(SCAN_CHUNK, size));
|
|
2743
|
+
for (let position = 0; position < size;) {
|
|
2744
|
+
const length = Math.min(buffer.length, size - position);
|
|
2745
|
+
const read = await new Promise((resolve, reject) => vfs.read(fd, buffer, 0, length, position, (error, bytesRead) => error ? reject(error) : resolve(bytesRead)));
|
|
2746
|
+
if (read === 0) break;
|
|
2747
|
+
scanner.push(buffer.subarray(0, read));
|
|
2748
|
+
position += read;
|
|
2749
|
+
}
|
|
2750
|
+
} finally {
|
|
2751
|
+
await close(fd);
|
|
2752
|
+
}
|
|
2753
|
+
return scanner.finish();
|
|
2754
|
+
}
|
|
2513
2755
|
};
|
|
2514
2756
|
};
|
|
2515
2757
|
/** The `SandboxFileSystem.Service` backed by the local {@link Vfs}. */
|
|
@@ -2637,10 +2879,12 @@ const layer$19 = (...drivers) => Layer.effect(Registry, make$16(drivers));
|
|
|
2637
2879
|
//#endregion
|
|
2638
2880
|
//#region src/sandbox/shell/host.ts
|
|
2639
2881
|
const DEFAULT_SHELL = "sh";
|
|
2882
|
+
const DEFAULT_FORCE_KILL_AFTER = Duration.seconds(1);
|
|
2640
2883
|
const layer$18 = (options = {}) => Layer.effect(shell_exports.Shell, Effect.gen(function* () {
|
|
2641
2884
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
2642
|
-
const
|
|
2643
|
-
|
|
2885
|
+
const forceKillAfter = options.forceKillAfter ?? DEFAULT_FORCE_KILL_AFTER;
|
|
2886
|
+
const spawn = (file, args, opts) => Effect.gen(function* () {
|
|
2887
|
+
const spawned = yield* spawner.spawn(ChildProcess.make(file, [...args], {
|
|
2644
2888
|
cwd: opts?.cwd,
|
|
2645
2889
|
env: {
|
|
2646
2890
|
...options.env,
|
|
@@ -2651,6 +2895,32 @@ const layer$18 = (options = {}) => Layer.effect(shell_exports.Shell, Effect.gen(
|
|
|
2651
2895
|
stdout: "pipe",
|
|
2652
2896
|
stderr: "pipe"
|
|
2653
2897
|
}));
|
|
2898
|
+
let openPipes = 2;
|
|
2899
|
+
const track = (stream) => stream.pipe(Stream.ensuring(Effect.sync(() => openPipes--)));
|
|
2900
|
+
const output = {
|
|
2901
|
+
stdout: track(spawned.stdout),
|
|
2902
|
+
stderr: track(spawned.stderr)
|
|
2903
|
+
};
|
|
2904
|
+
yield* Effect.addFinalizer((exit) => Exit.isSuccess(exit) ? Effect.void : Effect.gen(function* () {
|
|
2905
|
+
const running = yield* spawned.isRunning.pipe(Effect.orElseSucceed(() => false));
|
|
2906
|
+
if (!running && openPipes === 0) return;
|
|
2907
|
+
if (running) yield* spawned.kill({ killSignal: "SIGTERM" }).pipe(Effect.timeoutOption(forceKillAfter), Effect.flatMap(Option.match({
|
|
2908
|
+
onSome: () => Effect.void,
|
|
2909
|
+
onNone: () => spawned.kill({ killSignal: "SIGKILL" })
|
|
2910
|
+
})), Effect.ignore);
|
|
2911
|
+
yield* Effect.sync(() => {
|
|
2912
|
+
try {
|
|
2913
|
+
process.kill(-Number(spawned.pid), "SIGKILL");
|
|
2914
|
+
} catch {}
|
|
2915
|
+
});
|
|
2916
|
+
}));
|
|
2917
|
+
return {
|
|
2918
|
+
exitCode: spawned.exitCode,
|
|
2919
|
+
...output
|
|
2920
|
+
};
|
|
2921
|
+
});
|
|
2922
|
+
const run = (label, file, args, opts) => Effect.scoped(Effect.gen(function* () {
|
|
2923
|
+
const handle = yield* spawn(file, args, opts);
|
|
2654
2924
|
return yield* Effect.all({
|
|
2655
2925
|
exitCode: handle.exitCode,
|
|
2656
2926
|
stdout: handle.stdout.pipe(Stream.decodeText(), Stream.mkString),
|
|
@@ -2665,9 +2935,23 @@ const layer$18 = (options = {}) => Layer.effect(shell_exports.Shell, Effect.gen(
|
|
|
2665
2935
|
command: "",
|
|
2666
2936
|
cause: /* @__PURE__ */ new Error("execArgv requires a program")
|
|
2667
2937
|
})) : run(argv.join(" "), argv[0], argv.slice(1), opts));
|
|
2938
|
+
const stream = (command, opts) => Stream.unwrap(Effect.map(spawn(options.shell ?? DEFAULT_SHELL, ["-c", command], opts), (handle) => Stream.concat(Stream.merge(handle.stdout.pipe(Stream.map((bytes) => ({
|
|
2939
|
+
_tag: "stdout",
|
|
2940
|
+
bytes
|
|
2941
|
+
}))), handle.stderr.pipe(Stream.map((bytes) => ({
|
|
2942
|
+
_tag: "stderr",
|
|
2943
|
+
bytes
|
|
2944
|
+
})))), Stream.fromEffect(Effect.map(handle.exitCode, (exitCode) => ({
|
|
2945
|
+
_tag: "exit",
|
|
2946
|
+
exitCode
|
|
2947
|
+
})))))).pipe(Stream.mapError((cause) => new shell_exports.ShellError({
|
|
2948
|
+
command,
|
|
2949
|
+
cause
|
|
2950
|
+
})));
|
|
2668
2951
|
return shell_exports.Shell.of({
|
|
2669
2952
|
exec,
|
|
2670
|
-
execArgv
|
|
2953
|
+
execArgv,
|
|
2954
|
+
stream
|
|
2671
2955
|
});
|
|
2672
2956
|
}));
|
|
2673
2957
|
//#endregion
|
|
@@ -3052,7 +3336,8 @@ const make$14 = Effect.fn("Sandbox.Controller.make")(function* (options) {
|
|
|
3052
3336
|
id,
|
|
3053
3337
|
driver: driver_exports.SandboxDriver.Name.make("local"),
|
|
3054
3338
|
kind: "local",
|
|
3055
|
-
cwd: io_exports.SandboxIO.resolveMountCwd(hostDefaultCwd, mountOptions.cwd)
|
|
3339
|
+
cwd: io_exports.SandboxIO.resolveMountCwd(hostDefaultCwd, mountOptions.cwd),
|
|
3340
|
+
spillPath: io_exports.SandboxIO.DEFAULT_SPILL_PATH
|
|
3056
3341
|
};
|
|
3057
3342
|
else {
|
|
3058
3343
|
const row = yield* requireRow(id);
|
|
@@ -3074,7 +3359,8 @@ const make$14 = Effect.fn("Sandbox.Controller.make")(function* (options) {
|
|
|
3074
3359
|
id,
|
|
3075
3360
|
driver: attached.driver.name,
|
|
3076
3361
|
kind: row.kind,
|
|
3077
|
-
cwd: io_exports.SandboxIO.resolveMountCwd(attached.input.runtimeConfig.defaultCwd, mountOptions.cwd)
|
|
3362
|
+
cwd: io_exports.SandboxIO.resolveMountCwd(attached.input.runtimeConfig.defaultCwd, mountOptions.cwd),
|
|
3363
|
+
spillPath: attached.input.runtimeConfig.spillPath ?? io_exports.SandboxIO.DEFAULT_SPILL_PATH
|
|
3078
3364
|
};
|
|
3079
3365
|
const wasIdle = lease.held ? false : yield* gate.withPermits(1)(Effect.gen(function* () {
|
|
3080
3366
|
const latest = yield* requireRow(id);
|
|
@@ -3121,7 +3407,7 @@ const make$14 = Effect.fn("Sandbox.Controller.make")(function* (options) {
|
|
|
3121
3407
|
WHERE id = ${id}
|
|
3122
3408
|
`.pipe(Effect.orDie);
|
|
3123
3409
|
}
|
|
3124
|
-
return Layer.mergeAll(Layer.succeed(io_exports.SandboxIO.Current, identity), Layer.succeed(io_exports.SandboxIO.FileSystem, filesystem_exports.SandboxFileSystem.withCwd(fs, identity.cwd)), Layer.succeed(io_exports.SandboxIO.Shell, (0, shell_exports.withCwd)(shell, identity.cwd)));
|
|
3410
|
+
return Layer.mergeAll(Layer.succeed(io_exports.SandboxIO.Current, identity), Layer.succeed(io_exports.SandboxIO.FileSystem, filesystem_exports.SandboxFileSystem.withCwd(fs, identity.cwd)), Layer.succeed(io_exports.SandboxIO.Shell, (0, shell_exports.withCwd)(shell, identity.cwd))).pipe(io_exports.SandboxIO.withMutation);
|
|
3125
3411
|
}));
|
|
3126
3412
|
const mount = (id, mountOptions) => mountLayer(id, mountOptions);
|
|
3127
3413
|
const withMount = (id, use, mountOptions) => use.pipe(Effect.provide(mount(id, mountOptions)), Effect.scoped);
|
|
@@ -3857,17 +4143,6 @@ const make$9 = () => {
|
|
|
3857
4143
|
}) };
|
|
3858
4144
|
};
|
|
3859
4145
|
//#endregion
|
|
3860
|
-
//#region src/sandbox/sandbox.ts
|
|
3861
|
-
/**
|
|
3862
|
-
* Assemble a local backend into a sandbox with a **real host shell** — the
|
|
3863
|
-
* right choice whenever commands must invoke actual binaries (`git`, package
|
|
3864
|
-
* managers). For a sandbox whose shell should stay inside the VFS, use
|
|
3865
|
-
* `EnvBash.services` instead, which wires just-bash over the same tree.
|
|
3866
|
-
*/
|
|
3867
|
-
const services = (backend, identity) => Layer.provideMerge(io_exports.SandboxIO.mount(identity), Layer.provideMerge(Layer.merge(layer$21, layer$18()), backend));
|
|
3868
|
-
/** Default sandbox: the real OS filesystem and processes, mounted at `cwd`. */
|
|
3869
|
-
const defaultLayer$1 = (cwd) => services(layer$20(), io_exports.SandboxIO.host(cwd));
|
|
3870
|
-
//#endregion
|
|
3871
4146
|
//#region src/git/git.ts
|
|
3872
4147
|
var AppProcessError = class extends Schema.TaggedError()("AppProcessError", {
|
|
3873
4148
|
command: Schema.String,
|
|
@@ -4082,7 +4357,7 @@ var Service$11 = class extends Context.Service()("@codeworksh/harness/repo/repo/
|
|
|
4082
4357
|
* `.git` suffix and surrounding slashes stripped, scp form accepted.
|
|
4083
4358
|
* `file:` urls and empty input yield undefined.
|
|
4084
4359
|
*/
|
|
4085
|
-
function normalize$
|
|
4360
|
+
function normalize$3(url) {
|
|
4086
4361
|
const value = url.trim();
|
|
4087
4362
|
if (!value) return void 0;
|
|
4088
4363
|
try {
|
|
@@ -4124,7 +4399,7 @@ const layer$14 = Layer.effect(Service$11, Effect.gen(function* () {
|
|
|
4124
4399
|
const marker = posix.join(repo.store, MARKER$1);
|
|
4125
4400
|
const pinned = (yield* readFileSafe(fs, marker))?.trim() || void 0;
|
|
4126
4401
|
const url = yield* git.remote(repo.directory);
|
|
4127
|
-
const normalized = url === void 0 ? void 0 : normalize$
|
|
4402
|
+
const normalized = url === void 0 ? void 0 : normalize$3(url);
|
|
4128
4403
|
const remote = normalized === void 0 ? void 0 : {
|
|
4129
4404
|
id: fast(`git:${normalized}`),
|
|
4130
4405
|
name: posix.basename(normalized)
|
|
@@ -4351,16 +4626,13 @@ var location_exports = /* @__PURE__ */ __exportAll({
|
|
|
4351
4626
|
Info: () => Info$1,
|
|
4352
4627
|
Location: () => location_exports,
|
|
4353
4628
|
NotDirectoryError: () => NotDirectoryError,
|
|
4354
|
-
Ref: () => Ref$2,
|
|
4355
4629
|
Service: () => Service$8,
|
|
4356
|
-
defaultLayer: () => defaultLayer,
|
|
4357
4630
|
layer: () => layer$11,
|
|
4358
|
-
layerMounted: () => layerMounted
|
|
4359
|
-
layerWith: () => layerWith
|
|
4631
|
+
layerMounted: () => layerMounted
|
|
4360
4632
|
});
|
|
4361
|
-
const layer$11 = (
|
|
4633
|
+
const layer$11 = () => Layer.effect(Service, Effect.gen(function* () {
|
|
4362
4634
|
const instance = yield* io_exports.SandboxIO.Current;
|
|
4363
|
-
const directory = schema_exports$9.AbsolutePath.make(
|
|
4635
|
+
const directory = schema_exports$9.AbsolutePath.make(instance.cwd);
|
|
4364
4636
|
const resolved = yield* (yield* Service$9).resolveOrCreate(directory);
|
|
4365
4637
|
return Service.of(new Info({
|
|
4366
4638
|
directory: resolved.directory,
|
|
@@ -4373,9 +4645,7 @@ const layer$11 = (ref = {}) => Layer.effect(Service, Effect.gen(function* () {
|
|
|
4373
4645
|
* folded in. What remains is exactly the mount and the database — for callers
|
|
4374
4646
|
* that already hold both, such as the runner and `Session.create`.
|
|
4375
4647
|
*/
|
|
4376
|
-
const layerMounted = (
|
|
4377
|
-
const layerWith = (ref, sandbox) => layerMounted(ref).pipe(Layer.provide(sandbox), Layer.provide(defaultLayer$3));
|
|
4378
|
-
const defaultLayer = (ref, path) => layerWith(ref, defaultLayer$1(path));
|
|
4648
|
+
const layerMounted = () => layer$11().pipe(Layer.provide(layer$12.pipe(Layer.provide(Layer.mergeAll(layer$14, layer$13, layer$24)), Layer.provide(layer$15))));
|
|
4379
4649
|
//#endregion
|
|
4380
4650
|
//#region src/runner/run.ts
|
|
4381
4651
|
var run_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -4870,78 +5140,327 @@ const layer$9 = (definitions = Definitions) => Layer.sync(Service$4, () => {
|
|
|
4870
5140
|
});
|
|
4871
5141
|
});
|
|
4872
5142
|
//#endregion
|
|
4873
|
-
//#region src/
|
|
4874
|
-
var
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
5143
|
+
//#region src/settings/schema.ts
|
|
5144
|
+
var schema_exports$3 = /* @__PURE__ */ __exportAll({});
|
|
5145
|
+
import * as import__codeworksh_plugin_settings from "@codeworksh/plugin/settings";
|
|
5146
|
+
__reExport(schema_exports$3, import__codeworksh_plugin_settings);
|
|
5147
|
+
//#endregion
|
|
5148
|
+
//#region src/settings/merge.ts
|
|
5149
|
+
/**
|
|
5150
|
+
* A plain string-keyed record: what a settings patch merges, and what a plugin's `options` block
|
|
5151
|
+
* has to be. A Map, a typed array or a class instance is an object to `typeof` and not one of
|
|
5152
|
+
* these — `Object.freeze` alone does not tell them apart, and it throws on some of them.
|
|
5153
|
+
*/
|
|
5154
|
+
const isRecord$1 = (value) => Predicate.isObject(value) && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
5155
|
+
const copy = (value) => {
|
|
5156
|
+
if (Array.isArray(value)) return value.map(copy);
|
|
5157
|
+
return isRecord$1(value) ? merge({}, value) : value;
|
|
5158
|
+
};
|
|
5159
|
+
/** Null/undefined properties are absent, objects merge, and arrays replace. Never mutates inputs. */
|
|
5160
|
+
function merge(base, ...patches) {
|
|
5161
|
+
const result = /* @__PURE__ */ new Map();
|
|
5162
|
+
for (const patch of [base, ...patches]) {
|
|
5163
|
+
if (patch === void 0) continue;
|
|
5164
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
5165
|
+
if (value === null || value === void 0) continue;
|
|
5166
|
+
const previous = result.get(key);
|
|
5167
|
+
result.set(key, isRecord$1(previous) && isRecord$1(value) ? merge(previous, value) : copy(value));
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
return Object.fromEntries(result);
|
|
5171
|
+
}
|
|
5172
|
+
/** Normalize nullable properties before schema validation, without changing array positions. */
|
|
5173
|
+
const normalize$2 = copy;
|
|
5174
|
+
//#endregion
|
|
5175
|
+
//#region src/settings/settings.ts
|
|
5176
|
+
var settings_exports = /* @__PURE__ */ __exportAll({
|
|
5177
|
+
Service: () => Service$3,
|
|
5178
|
+
Settings: () => settings_exports,
|
|
5179
|
+
SettingsError: () => SettingsError,
|
|
5180
|
+
defaults: () => schema_exports$3.defaults,
|
|
4878
5181
|
layer: () => layer$8,
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
5182
|
+
load: () => load$3,
|
|
5183
|
+
modules: () => modules,
|
|
5184
|
+
parse: () => parse$4,
|
|
5185
|
+
paths: () => paths,
|
|
5186
|
+
projectRoot: () => projectRoot,
|
|
5187
|
+
userConfigDir: () => userConfigDir
|
|
4885
5188
|
});
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
5189
|
+
/**
|
|
5190
|
+
* An app-level `--user-config-dir` as the absolute directory it names: `~` is the user's home, and
|
|
5191
|
+
* a relative spelling is relative to `hostCwd` -- never to a session's `hostDir`, which is not
|
|
5192
|
+
* where the flag was typed.
|
|
5193
|
+
*/
|
|
5194
|
+
const userConfigDir = (raw, hostCwd) => hostPath.resolve(hostCwd, expandTilde(raw, hostPath));
|
|
5195
|
+
/** The settings files a directory may hold, in the order it is searched. */
|
|
5196
|
+
const settingsIn = (directory) => [hostPath.join(directory, "settings.jsonc"), hostPath.join(directory, "settings.json")];
|
|
5197
|
+
/** A directory and every ancestor above it, nearest first. */
|
|
5198
|
+
const ancestors$1 = (from) => {
|
|
5199
|
+
const chain = [];
|
|
5200
|
+
for (let directory = from;; directory = hostPath.dirname(directory)) {
|
|
5201
|
+
chain.push(directory);
|
|
5202
|
+
if (hostPath.dirname(directory) === directory) return chain;
|
|
5203
|
+
}
|
|
5204
|
+
};
|
|
5205
|
+
const isDirectory = (path) => fileSystem.stat(path).pipe(Effect.map((info) => info.type === "Directory"), Effect.orElseSucceed(() => false));
|
|
5206
|
+
/**
|
|
5207
|
+
* The project a directory is in: the nearest ancestor holding a `.codework` **directory**.
|
|
5208
|
+
*
|
|
5209
|
+
* This is git's rule. `~/workspace/app/.codework` makes `~/workspace/app` a project, and
|
|
5210
|
+
* `~/workspace/app/packages/fubar` is inside it. Nearest wins outright -- the walk stops at the
|
|
5211
|
+
* first marker, so an inner project is never merged with an outer one.
|
|
5212
|
+
*
|
|
5213
|
+
* The marker is the directory, not a file in it. An empty `.codework/` is still a project root,
|
|
5214
|
+
* which is the point: creating the directory is how a person says "a project begins here", and it
|
|
5215
|
+
* has to mean that before any settings exist. It also makes the question one `stat` per ancestor
|
|
5216
|
+
* whose answer does not change as files come and go inside.
|
|
5217
|
+
*
|
|
5218
|
+
* `<home>` is skipped. `~/.codework` is the user layer, not a project; without this, running
|
|
5219
|
+
* anywhere under `$HOME` would find it as one and read the user layer twice -- and since plugin
|
|
5220
|
+
* entries accumulate across layers, every user plugin would load twice.
|
|
5221
|
+
*/
|
|
5222
|
+
const projectRoot = Effect.fn("Settings.projectRoot")(function* (from, home) {
|
|
5223
|
+
const userConfig = hostPath.resolve(home);
|
|
5224
|
+
for (const directory of ancestors$1(hostPath.resolve(from))) {
|
|
5225
|
+
const marker = hostPath.join(directory, appConfigDir);
|
|
5226
|
+
if (hostPath.resolve(marker) === userConfig) continue;
|
|
5227
|
+
if (yield* isDirectory(marker)) return directory;
|
|
5228
|
+
}
|
|
5229
|
+
});
|
|
5230
|
+
/**
|
|
5231
|
+
* Ordered layers, each a group of candidates where the first file that exists is selected -- a
|
|
5232
|
+
* group never contributes more than one file.
|
|
5233
|
+
*
|
|
5234
|
+
* `.jsonc` is the canonical spelling: these files are written by hand and read as JSONC, so the
|
|
5235
|
+
* extension says what the file is and an editor validates it as such. `.json` remains a fallback
|
|
5236
|
+
* everywhere, tried second, because the parser reads either and a file already named that must
|
|
5237
|
+
* keep working.
|
|
5238
|
+
*
|
|
5239
|
+
* `root` is an already-discovered project root ({@link projectRoot}), not a directory to search
|
|
5240
|
+
* from: the walk is a `stat` per ancestor and belongs with the other I/O. `undefined` contributes
|
|
5241
|
+
* no project layer at all, which is what a session with no host directory gets.
|
|
5242
|
+
*/
|
|
5243
|
+
function paths(input) {
|
|
5244
|
+
return [settingsIn(input.userConfigDir ?? input.home), input.root === void 0 ? [] : settingsIn(hostPath.join(input.root, appConfigDir))];
|
|
5245
|
+
}
|
|
5246
|
+
var SettingsError = class extends Schema.TaggedError()("SettingsError", {
|
|
4892
5247
|
path: Schema.String,
|
|
5248
|
+
reason: Schema.Literals([
|
|
5249
|
+
"read",
|
|
5250
|
+
"parse",
|
|
5251
|
+
"decode"
|
|
5252
|
+
]),
|
|
4893
5253
|
detail: Schema.String
|
|
4894
5254
|
}) {
|
|
4895
5255
|
get message() {
|
|
4896
|
-
return
|
|
5256
|
+
return `${this.path}: ${this.reason}: ${this.detail}`;
|
|
4897
5257
|
}
|
|
4898
5258
|
};
|
|
4899
|
-
/**
|
|
4900
|
-
const
|
|
4901
|
-
const
|
|
4902
|
-
|
|
4903
|
-
const path = Model$1.catalogPath();
|
|
4904
|
-
return {
|
|
4905
|
-
path,
|
|
4906
|
-
pinned: path !== own
|
|
4907
|
-
};
|
|
5259
|
+
/** `line:column`, 1-based, for a parser offset into the source. */
|
|
5260
|
+
const at = (source, offset) => {
|
|
5261
|
+
const before = source.slice(0, offset).split("\n");
|
|
5262
|
+
return `${before.length}:${(before.at(-1)?.length ?? 0) + 1}`;
|
|
4908
5263
|
};
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
const
|
|
4925
|
-
|
|
4926
|
-
if (options.force !== true && !(yield* stale(path))) return {
|
|
5264
|
+
/**
|
|
5265
|
+
* Decode failures name the field, and explain it only with a message a schema check wrote. Effect's
|
|
5266
|
+
* defaults quote the rejected value, and in a settings file that value can be a secret, so any
|
|
5267
|
+
* other failure reads "invalid value".
|
|
5268
|
+
*/
|
|
5269
|
+
const decodeFormatter = SchemaIssue.makeFormatterStandardSchemaV1({ leafHook: (issue) => {
|
|
5270
|
+
const message = "annotations" in issue ? issue.annotations?.message : void 0;
|
|
5271
|
+
return typeof message === "string" ? message.replaceAll(/\s*\n\s*/g, " ") : "invalid value";
|
|
5272
|
+
} });
|
|
5273
|
+
const parse$4 = Effect.fn("Settings.parse")(function* (path, source) {
|
|
5274
|
+
const errors = [];
|
|
5275
|
+
const json = parse(source, errors, {
|
|
5276
|
+
allowTrailingComma: true,
|
|
5277
|
+
disallowComments: false
|
|
5278
|
+
});
|
|
5279
|
+
const first = errors[0];
|
|
5280
|
+
if (first !== void 0) return yield* new SettingsError({
|
|
4927
5281
|
path,
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
5282
|
+
reason: "parse",
|
|
5283
|
+
detail: `${printParseErrorCode(first.error)} at ${at(source, first.offset)}`
|
|
5284
|
+
});
|
|
5285
|
+
const document = normalize$2(json);
|
|
5286
|
+
if (Predicate.isObject(document) && Predicate.isObject(json) && "plugins" in json) Object.assign(document, { plugins: json.plugins });
|
|
5287
|
+
return yield* Schema.decodeUnknownEffect(schema_exports$3.Patch)(document).pipe(Effect.mapError((error) => new SettingsError({
|
|
4932
5288
|
path,
|
|
4933
|
-
|
|
4934
|
-
|
|
5289
|
+
reason: "decode",
|
|
5290
|
+
detail: decodeFormatter(error.issue).issues.map((issue) => `${issue.path?.map((part) => typeof part === "object" ? String(part.key) : part).join(".") ?? "settings"}: ${issue.message}`).join("; ")
|
|
5291
|
+
})));
|
|
4935
5292
|
});
|
|
5293
|
+
var Service$3 = class extends Context.Service()("@codeworksh/harness/settings/settings/Service") {};
|
|
4936
5294
|
/**
|
|
4937
|
-
*
|
|
4938
|
-
*
|
|
4939
|
-
*
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
5295
|
+
* Anchor relative plugin entries to the file that declared them.
|
|
5296
|
+
*
|
|
5297
|
+
* A reference is otherwise resolved against the host startup directory, which is right for
|
|
5298
|
+
* a project file sitting in it and meaningless for `~/.codework/settings.json`, where
|
|
5299
|
+
* `./plugins/x.ts` would name a different file in every project the process is started in.
|
|
5300
|
+
* `file:` URLs, package specs and package names are left exactly as written, and a `plugin`
|
|
5301
|
+
* key is an ID rather than a location. A configuration entry's `package` is anchored like a
|
|
5302
|
+
* module entry, so a relative path names the same module in both spellings.
|
|
5303
|
+
*/
|
|
5304
|
+
const anchored = (entry, file) => {
|
|
5305
|
+
const directory = hostPath.dirname(file);
|
|
5306
|
+
const resolve = (reference) => reference.startsWith("./") || reference.startsWith("../") ? hostPath.resolve(directory, reference) : reference;
|
|
5307
|
+
if (typeof entry === "string") return resolve(entry);
|
|
5308
|
+
return "package" in entry ? {
|
|
5309
|
+
...entry,
|
|
5310
|
+
package: resolve(entry.package)
|
|
5311
|
+
} : entry;
|
|
5312
|
+
};
|
|
5313
|
+
const anchor$1 = (patch, file) => patch.plugins === void 0 ? patch : {
|
|
5314
|
+
...patch,
|
|
5315
|
+
plugins: patch.plugins.map((entry) => anchored(entry, file))
|
|
5316
|
+
};
|
|
5317
|
+
const attempt = (path) => fileSystem.readFileString(path).pipe(Effect.mapError((error) => new SettingsError({
|
|
5318
|
+
path,
|
|
5319
|
+
reason: "read",
|
|
5320
|
+
detail: error.reason._tag
|
|
5321
|
+
})), Effect.flatMap((source) => parse$4(path, source)));
|
|
5322
|
+
/**
|
|
5323
|
+
* One read of every layer. Exported for the harness constructor, which needs the plugin
|
|
5324
|
+
* selection before any layer is built; everything else goes through `Service`.
|
|
5325
|
+
*/
|
|
5326
|
+
const load$3 = Effect.fn("Settings.load")(function* (options) {
|
|
5327
|
+
const root = options.hostDir === void 0 ? void 0 : yield* projectRoot(options.hostDir, options.home);
|
|
5328
|
+
const files = paths({
|
|
5329
|
+
home: options.home,
|
|
5330
|
+
root,
|
|
5331
|
+
...options.userConfigDir === void 0 ? {} : { userConfigDir: options.userConfigDir }
|
|
5332
|
+
});
|
|
5333
|
+
let settings = merge(schema_exports$3.defaults);
|
|
5334
|
+
let declared = schema_exports$3.defaults.declared;
|
|
5335
|
+
let models = schema_exports$3.defaults.models;
|
|
5336
|
+
for (const group of files) for (const path of group) {
|
|
5337
|
+
const result = yield* Effect.result(attempt(path));
|
|
5338
|
+
if (Result.isSuccess(result)) {
|
|
5339
|
+
const { models: entries, ...patch } = anchor$1(result.success, path);
|
|
5340
|
+
settings = merge(settings, patch);
|
|
5341
|
+
if (entries !== void 0) models = [...models, ...entries.map((entry) => ({
|
|
5342
|
+
entry,
|
|
5343
|
+
file: path
|
|
5344
|
+
}))];
|
|
5345
|
+
if (result.success.plugins !== void 0) declared = [...declared, ...result.success.plugins.map((written) => ({
|
|
5346
|
+
written,
|
|
5347
|
+
entry: anchored(written, path),
|
|
5348
|
+
file: path
|
|
5349
|
+
}))];
|
|
5350
|
+
break;
|
|
5351
|
+
}
|
|
5352
|
+
const error = result.failure;
|
|
5353
|
+
if (error.reason === "read" && error.detail === "NotFound") continue;
|
|
5354
|
+
return yield* error;
|
|
5355
|
+
}
|
|
5356
|
+
return {
|
|
5357
|
+
...settings,
|
|
5358
|
+
plugins: declared.map((one) => one.entry),
|
|
5359
|
+
declared,
|
|
5360
|
+
models
|
|
5361
|
+
};
|
|
5362
|
+
});
|
|
5363
|
+
/**
|
|
5364
|
+
* Each module reference once, owned by the **last** file to declare it (D6).
|
|
5365
|
+
*
|
|
5366
|
+
* The owner is not bookkeeping: its `.npmrc` chain keys the store artifact. The CLI that installs
|
|
5367
|
+
* a reference and the runtime that loads it must pick the same owner, or `plugin update` refreshes
|
|
5368
|
+
* an artifact the runtime never reads. Only a string entry loads a module, so only a string entry
|
|
5369
|
+
* owns one -- a `{ package }` patch in another file configures it and must not move its registry.
|
|
5370
|
+
* A reference keeps the position of its first declaration.
|
|
5371
|
+
*/
|
|
5372
|
+
const modules = (declared) => {
|
|
5373
|
+
const owners = /* @__PURE__ */ new Map();
|
|
5374
|
+
for (const one of declared) if (typeof one.entry === "string") owners.set(one.entry, one);
|
|
5375
|
+
return owners;
|
|
5376
|
+
};
|
|
5377
|
+
const layer$8 = (options = {}) => Layer.effect(Service$3, Effect.gen(function* () {
|
|
5378
|
+
const global = yield* Service$17;
|
|
5379
|
+
return Service$3.of({ load: (hostDir) => load$3({
|
|
5380
|
+
...options,
|
|
5381
|
+
home: global.home,
|
|
5382
|
+
hostDir
|
|
5383
|
+
}) });
|
|
5384
|
+
}));
|
|
5385
|
+
//#endregion
|
|
5386
|
+
//#region src/model/catalog.ts
|
|
5387
|
+
var catalog_exports = /* @__PURE__ */ __exportAll({
|
|
5388
|
+
Credentials: () => Credentials,
|
|
5389
|
+
ModelCatalog: () => catalog_exports,
|
|
5390
|
+
RefreshError: () => RefreshError,
|
|
5391
|
+
available: () => available$1,
|
|
5392
|
+
effective: () => effective,
|
|
5393
|
+
entry: () => entry,
|
|
5394
|
+
filename: () => filename,
|
|
5395
|
+
fromSettings: () => fromSettings,
|
|
5396
|
+
layer: () => layer$7,
|
|
5397
|
+
loadError: () => loadError,
|
|
5398
|
+
models: () => models,
|
|
5399
|
+
oauthStorage: () => oauthStorage,
|
|
5400
|
+
providers: () => providers,
|
|
5401
|
+
refresh: () => refresh$1,
|
|
5402
|
+
sync: () => sync,
|
|
5403
|
+
ttlConfig: () => ttlConfig
|
|
5404
|
+
});
|
|
5405
|
+
const filename = "models.gen.json";
|
|
5406
|
+
/** How old the catalog may get before a check downloads a new one. */
|
|
5407
|
+
const ttlConfig = Config.Duration("CODEWORK_MODELS_TTL").pipe(Config.withDefault(Duration.minutes(15)));
|
|
5408
|
+
/** How often a server looks at the file. A look is a `stat`; only a stale file costs a download. */
|
|
5409
|
+
const checkEvery = Duration.minutes(1);
|
|
5410
|
+
var RefreshError = class extends Schema.TaggedError()("ModelCatalog.RefreshError", {
|
|
5411
|
+
path: Schema.String,
|
|
5412
|
+
detail: Schema.String
|
|
5413
|
+
}) {
|
|
5414
|
+
get message() {
|
|
5415
|
+
return `failed to refresh the model catalog at ${this.path}: ${this.detail}`;
|
|
5416
|
+
}
|
|
5417
|
+
};
|
|
5418
|
+
/** Point aikit at `<home>/models.gen.json`, which `CODEWORK_MODELS_FILE` overrides. */
|
|
5419
|
+
const use = (home) => {
|
|
5420
|
+
const own = hostPath.resolve(home, filename);
|
|
5421
|
+
Model.configureCatalog(own);
|
|
5422
|
+
const path = Model.catalogPath();
|
|
5423
|
+
return {
|
|
5424
|
+
path,
|
|
5425
|
+
pinned: path !== own
|
|
5426
|
+
};
|
|
5427
|
+
};
|
|
5428
|
+
const modified = (path) => fileSystem.stat(path).pipe(Effect.map((info) => Option.map(info.mtime, (date) => date.getTime())), Effect.orElseSucceed(() => Option.none()));
|
|
5429
|
+
const stale = Effect.fn("ModelCatalog.stale")(function* (path) {
|
|
5430
|
+
const mtime = yield* modified(path);
|
|
5431
|
+
if (Option.isNone(mtime)) return true;
|
|
5432
|
+
const ttl = yield* ttlConfig;
|
|
5433
|
+
return (yield* Clock.currentTimeMillis) - mtime.value >= Duration.toMillis(ttl);
|
|
5434
|
+
});
|
|
5435
|
+
const download$1 = (path) => Effect.tryPromise({
|
|
5436
|
+
try: () => generateModels({ path }),
|
|
5437
|
+
catch: (cause) => new RefreshError({
|
|
5438
|
+
path,
|
|
5439
|
+
detail: cause instanceof Error ? cause.message : String(cause)
|
|
5440
|
+
})
|
|
5441
|
+
});
|
|
5442
|
+
/** Download a new catalog for the home when it is missing, older than the TTL, or `force` asks. */
|
|
5443
|
+
const refresh$1 = Effect.fn("ModelCatalog.refresh")(function* (home, options = {}) {
|
|
5444
|
+
const { path } = use(home);
|
|
5445
|
+
if (options.force !== true && !(yield* stale(path))) return {
|
|
5446
|
+
path,
|
|
5447
|
+
refreshed: false
|
|
5448
|
+
};
|
|
5449
|
+
yield* download$1(path);
|
|
5450
|
+
return {
|
|
5451
|
+
path,
|
|
5452
|
+
refreshed: true
|
|
5453
|
+
};
|
|
5454
|
+
});
|
|
5455
|
+
/**
|
|
5456
|
+
* The automatic check: refresh a stale catalog unless it is pinned, and never fail. A failed
|
|
5457
|
+
* download leaves the old file in place; a missing one surfaces later as a
|
|
5458
|
+
* `Runner.ModelCatalogError` from whatever needed it.
|
|
5459
|
+
*/
|
|
5460
|
+
const check$1 = Effect.fn("ModelCatalog.check")(function* ({ path, pinned }) {
|
|
5461
|
+
if (pinned || !(yield* stale(path))) return;
|
|
5462
|
+
yield* download$1(path);
|
|
5463
|
+
});
|
|
4945
5464
|
const quietly = (effect) => effect.pipe(Effect.asVoid, Effect.catch((error) => Effect.logWarning(`model catalog: ${error.message}`)));
|
|
4946
5465
|
/** Check the home's catalog once, for a command that runs and exits; returns the file aikit reads. */
|
|
4947
5466
|
const sync = Effect.fn("ModelCatalog.sync")(function* (home) {
|
|
@@ -4954,7 +5473,7 @@ const sync = Effect.fn("ModelCatalog.sync")(function* (home) {
|
|
|
4954
5473
|
* moved -- refreshed here, by `codework models refresh`, or by hand -- is reloaded, so the
|
|
4955
5474
|
* process never serves a catalog older than what is on disk.
|
|
4956
5475
|
*/
|
|
4957
|
-
const layer$
|
|
5476
|
+
const layer$7 = (options) => Layer.effectDiscard(Effect.gen(function* () {
|
|
4958
5477
|
const target = use(options.home);
|
|
4959
5478
|
yield* quietly(check$1(target));
|
|
4960
5479
|
if (!options.watch) return;
|
|
@@ -4963,7 +5482,7 @@ const layer$8 = (options) => Layer.effectDiscard(Effect.gen(function* () {
|
|
|
4963
5482
|
const tick = Effect.gen(function* () {
|
|
4964
5483
|
yield* quietly(check$1(target));
|
|
4965
5484
|
const mtime = yield* current;
|
|
4966
|
-
if (mtime !== (yield* Ref.getAndSet(seen, mtime))) Model
|
|
5485
|
+
if (mtime !== (yield* Ref.getAndSet(seen, mtime))) Model.reloadCatalog();
|
|
4967
5486
|
});
|
|
4968
5487
|
yield* Effect.forkScoped(tick.pipe(Effect.repeat(Schedule.spaced(checkEvery))));
|
|
4969
5488
|
}));
|
|
@@ -4980,79 +5499,112 @@ const loadError = (cause) => {
|
|
|
4980
5499
|
});
|
|
4981
5500
|
};
|
|
4982
5501
|
const catalogError = (cause) => loadError(cause) ?? new ModelCatalogError({
|
|
4983
|
-
path: Model
|
|
5502
|
+
path: Model.catalogPath(),
|
|
4984
5503
|
reason: "unreadable",
|
|
4985
5504
|
detail: cause instanceof Error ? cause.message : "failed to load the model catalog"
|
|
4986
5505
|
});
|
|
4987
5506
|
const models = Effect.tryPromise({
|
|
4988
|
-
try: () => Model
|
|
5507
|
+
try: () => Model.getBuiltInModels(),
|
|
4989
5508
|
catch: catalogError
|
|
4990
5509
|
});
|
|
4991
5510
|
const providers = Effect.tryPromise({
|
|
4992
|
-
try: () => Model
|
|
5511
|
+
try: () => Model.getProviders(),
|
|
4993
5512
|
catch: catalogError
|
|
4994
5513
|
});
|
|
4995
|
-
//#endregion
|
|
4996
|
-
//#region src/plugin/plugin.ts
|
|
4997
|
-
var plugin_exports$1 = /* @__PURE__ */ __exportAll({});
|
|
4998
|
-
import * as import__codeworksh_plugin_plugin from "@codeworksh/plugin/plugin";
|
|
4999
|
-
__reExport(plugin_exports$1, import__codeworksh_plugin_plugin);
|
|
5000
|
-
//#endregion
|
|
5001
|
-
//#region src/plugin/builtin/prompt/default.ts
|
|
5002
|
-
/** The default coding-agent foundation, used unless `promptCustom` replaces it. */
|
|
5003
|
-
const foundation = `You are an expert coding assistant operating inside codework, a coding agent harness.`;
|
|
5004
5514
|
/**
|
|
5005
|
-
*
|
|
5006
|
-
*
|
|
5007
|
-
*
|
|
5008
|
-
* earns its place only if a model measurably behaves worse without it.
|
|
5515
|
+
* Where this process reads OAuth logins: `<home>/aikit/auth.json` when a home was chosen,
|
|
5516
|
+
* otherwise `undefined`, which lets aikit resolve it (`CODEWORK_CREDENTIALS`, then the default home).
|
|
5517
|
+
* One value, so the model calls and {@link available} can never read different files.
|
|
5009
5518
|
*/
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
const
|
|
5519
|
+
var Credentials = class extends Context.Service()("@codeworksh/harness/model/catalog/Credentials") {};
|
|
5520
|
+
/** The storage options an aikit OAuth store takes for {@link Credentials.authFile}. */
|
|
5521
|
+
const oauthStorage = (authFile) => authFile === void 0 ? {} : { path: authFile };
|
|
5522
|
+
/** Whether an OAuth login is stored for the protocol. Reads the file; never refreshes a token. */
|
|
5523
|
+
const loggedIn = (protocol, authFile) => {
|
|
5524
|
+
const storage = oauthStorage(authFile);
|
|
5525
|
+
const store = protocol === "openai-codex" ? new JsonOpenAICodexAuthStorage(storage) : protocol === "github-copilot" ? new JsonGitHubCopilotAuthStorage(storage) : void 0;
|
|
5526
|
+
if (store === void 0) return Effect.succeed(false);
|
|
5527
|
+
return Effect.tryPromise(() => store.get()).pipe(Effect.map((stored) => stored !== void 0), Effect.orElseSucceed(() => false));
|
|
5528
|
+
};
|
|
5017
5529
|
/**
|
|
5018
|
-
*
|
|
5019
|
-
*
|
|
5020
|
-
*
|
|
5021
|
-
* Tool order is bucket order, which is the same order the index renders in and the
|
|
5022
|
-
* same order the provider receives definitions in -- one ordering, not three.
|
|
5530
|
+
* A settings entry as the catalog reads it. Settings entries are read exactly like generated ones;
|
|
5531
|
+
* the one difference is `provider.key`, written `"${NAME}"`, which becomes the variable name aikit
|
|
5532
|
+
* looks up when it calls the model.
|
|
5023
5533
|
*/
|
|
5024
|
-
const
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5534
|
+
const fromSettings = (entry) => entry.provider.key === void 0 ? entry : {
|
|
5535
|
+
...entry,
|
|
5536
|
+
provider: {
|
|
5537
|
+
...entry.provider,
|
|
5538
|
+
key: (0, schema_exports$3.keyName)(entry.provider.key)
|
|
5539
|
+
}
|
|
5540
|
+
};
|
|
5541
|
+
/** The settings entry for a model, if one declares it: the last declaration wins. */
|
|
5542
|
+
const entry = (declared, provider, id) => {
|
|
5543
|
+
const found = declared.findLast((one) => one.entry.provider.id === provider && one.entry.id === id);
|
|
5544
|
+
return found === void 0 ? void 0 : fromSettings(found.entry);
|
|
5034
5545
|
};
|
|
5035
5546
|
/**
|
|
5036
|
-
* The
|
|
5547
|
+
* The generated catalog with settings entries appended in layer order: a new provider or model is
|
|
5548
|
+
* added, and an entry for an existing one replaces it, as a later entry in a generated file would.
|
|
5549
|
+
*/
|
|
5550
|
+
const effective = Effect.fn("ModelCatalog.effective")(function* (declared) {
|
|
5551
|
+
const catalog = { ...yield* models };
|
|
5552
|
+
for (const { entry: one } of declared) catalog[one.provider.id] = {
|
|
5553
|
+
...catalog[one.provider.id],
|
|
5554
|
+
[one.id]: fromSettings(one)
|
|
5555
|
+
};
|
|
5556
|
+
return catalog;
|
|
5557
|
+
});
|
|
5558
|
+
const oauthProtocols = /* @__PURE__ */ new Set(["openai-codex", "github-copilot"]);
|
|
5559
|
+
/**
|
|
5560
|
+
* The effective catalog's providers this process has credentials for, optionally only those
|
|
5561
|
+
* credentialed one way. `hostDir` selects the project whose settings entries apply; without it,
|
|
5562
|
+
* only the user's.
|
|
5037
5563
|
*
|
|
5038
|
-
* `
|
|
5039
|
-
*
|
|
5564
|
+
* `apiKey`: the provider's `key` or `env` names are set in the host environment (they include the
|
|
5565
|
+
* OAuth providers' env tokens). `oauth`: a login is stored in the {@link Credentials} file. `none`:
|
|
5566
|
+
* the provider names no key and isn't an OAuth provider. A provider with a key and a login reports
|
|
5567
|
+
* `apiKey`, the one a model call uses first.
|
|
5568
|
+
*
|
|
5569
|
+
* Presence, not validity: nothing is sent over the network, so a revoked key still counts. A
|
|
5570
|
+
* client showing a model picker should still add the session's current model.
|
|
5040
5571
|
*/
|
|
5041
|
-
const
|
|
5042
|
-
const
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
const
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5572
|
+
const available$1 = Effect.fn("ModelCatalog.available")(function* (filter = {}) {
|
|
5573
|
+
const { authFile } = yield* Credentials;
|
|
5574
|
+
const settings = yield* Service$3;
|
|
5575
|
+
const catalog = yield* effective((yield* settings.load(filter.hostDir)).models);
|
|
5576
|
+
const usable = [];
|
|
5577
|
+
for (const [id, entries] of Object.entries(catalog)) {
|
|
5578
|
+
const first = entries === void 0 ? void 0 : Object.values(entries)[0];
|
|
5579
|
+
if (entries === void 0 || first === void 0) continue;
|
|
5580
|
+
const keyless = first.provider.key === void 0 && first.provider.env.length === 0 && !oauthProtocols.has(first.protocol);
|
|
5581
|
+
const credential = getEnvApiKey(first.provider) !== void 0 ? "apiKey" : (yield* loggedIn(first.protocol, authFile)) ? "oauth" : keyless ? "none" : void 0;
|
|
5582
|
+
if (credential === void 0 || filter.credential !== void 0 && filter.credential !== credential) continue;
|
|
5583
|
+
usable.push({
|
|
5584
|
+
id,
|
|
5585
|
+
credential,
|
|
5586
|
+
models: entries
|
|
5587
|
+
});
|
|
5588
|
+
}
|
|
5589
|
+
return usable;
|
|
5590
|
+
});
|
|
5591
|
+
//#endregion
|
|
5592
|
+
//#region src/plugin/plugin.ts
|
|
5593
|
+
var plugin_exports$1 = /* @__PURE__ */ __exportAll({});
|
|
5594
|
+
import * as import__codeworksh_plugin_plugin from "@codeworksh/plugin/plugin";
|
|
5595
|
+
__reExport(plugin_exports$1, import__codeworksh_plugin_plugin);
|
|
5596
|
+
//#endregion
|
|
5597
|
+
//#region src/plugin/builtin/prompt/default.ts
|
|
5598
|
+
/** The default prompt foundation, used unless `promptCustom` replaces it. */
|
|
5599
|
+
const foundation = "You are an expert coding assistant operating inside codework, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
|
|
5600
|
+
/** Every line here is spent on every request; keep it short. */
|
|
5601
|
+
const standingRules = ["Be concise in your responses", "Show file paths clearly when working with files"];
|
|
5602
|
+
/** Bash stands in for file tools only until dedicated ones are registered. */
|
|
5603
|
+
const toolRules = (names) => names.has("bash") && ![
|
|
5604
|
+
"grep",
|
|
5605
|
+
"find",
|
|
5606
|
+
"ls"
|
|
5607
|
+
].some((name) => names.has(name)) ? ["Use bash for file operations like ls, rg, find"] : [];
|
|
5056
5608
|
/**
|
|
5057
5609
|
* A caller slot, awaited lazily. A throw or rejection fails the snapshot, which the
|
|
5058
5610
|
* host attributes to this plugin's id.
|
|
@@ -5063,13 +5615,92 @@ const defaultPromptPlugin = (0, plugin_exports$1.define)({
|
|
|
5063
5615
|
kind: "prompt",
|
|
5064
5616
|
setup: Effect.fn("DefaultPromptPlugin.setup")(function* (ctx) {
|
|
5065
5617
|
const custom = yield* slot(ctx, ctx.config.promptCustom);
|
|
5066
|
-
const append = yield* slot(ctx, ctx.config.promptSystemAppend);
|
|
5067
|
-
ctx.plugin.prompt
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5618
|
+
const append = (yield* slot(ctx, ctx.config.promptSystemAppend))?.trim();
|
|
5619
|
+
const prompt = ctx.plugin.prompt;
|
|
5620
|
+
prompt.foundation.set(custom ?? foundation);
|
|
5621
|
+
const names = new Set(ctx.plugin.tools.list().map((tool) => tool.name));
|
|
5622
|
+
for (const rule of [...toolRules(names), ...standingRules]) prompt.sections.append(Section.Rules, rule);
|
|
5623
|
+
if (append !== void 0 && append.length > 0) prompt.sections.append(Section.Addendum, append);
|
|
5624
|
+
})
|
|
5625
|
+
});
|
|
5626
|
+
//#endregion
|
|
5627
|
+
//#region src/plugin/builtin/prompt/instruction.ts
|
|
5628
|
+
/** First readable match per directory wins. */
|
|
5629
|
+
const names = [
|
|
5630
|
+
"AGENTS.override.md",
|
|
5631
|
+
"CLAUDE.override.md",
|
|
5632
|
+
"AGENTS.md",
|
|
5633
|
+
"AGENTS.MD",
|
|
5634
|
+
"CLAUDE.md",
|
|
5635
|
+
"CLAUDE.MD"
|
|
5636
|
+
];
|
|
5637
|
+
const content = (raw) => {
|
|
5638
|
+
const text = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
|
|
5639
|
+
return text.trim().length === 0 ? void 0 : text;
|
|
5640
|
+
};
|
|
5641
|
+
/** `directory` and every ancestor up to the sandbox root, closest first. */
|
|
5642
|
+
const ancestors = (directory) => {
|
|
5643
|
+
const chain = [];
|
|
5644
|
+
for (let current = directory;; current = posix.dirname(current)) {
|
|
5645
|
+
chain.push(current);
|
|
5646
|
+
if (posix.dirname(current) === current) return chain;
|
|
5647
|
+
}
|
|
5648
|
+
};
|
|
5649
|
+
/**
|
|
5650
|
+
* Which of `paths` are files. Absence is the expected answer and stays silent; any other failure
|
|
5651
|
+
* (permissions, a remote blip) is logged so a dropped file is not a mystery, then counts as absent.
|
|
5652
|
+
*/
|
|
5653
|
+
const present$1 = (fs, paths) => Effect.forEach(paths, (path) => fs.stat(path).pipe(Effect.map((stat) => stat.isFile), Effect.catch((error) => filesystem_exports.SandboxFileSystem.isNotFoundError(error.cause) ? Effect.succeed(false) : Effect.logWarning(`could not stat instruction file ${path}`, error.cause).pipe(Effect.as(false)))), { concurrency: "unbounded" }).pipe(Effect.map((hits) => new Set(paths.filter((_, index) => hits[index]))));
|
|
5654
|
+
/** A file that stats but does not read falls through to the next candidate. */
|
|
5655
|
+
const first = Effect.fnUntraced(function* (fs, directory, files) {
|
|
5656
|
+
for (const name of names) {
|
|
5657
|
+
const path = posix.join(directory, name);
|
|
5658
|
+
if (!files.has(path)) continue;
|
|
5659
|
+
const raw = yield* fs.readFile(path).pipe(Effect.tapError((error) => Effect.logWarning(`could not read instruction file ${path}`, error.cause)), Effect.option);
|
|
5660
|
+
if (Option.isSome(raw)) return {
|
|
5661
|
+
name,
|
|
5662
|
+
path,
|
|
5663
|
+
content: content(raw.value)
|
|
5664
|
+
};
|
|
5665
|
+
}
|
|
5666
|
+
});
|
|
5667
|
+
/** Only the path is escaped; file content goes in verbatim. */
|
|
5668
|
+
const attribute = (value) => value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<");
|
|
5669
|
+
/**
|
|
5670
|
+
* The main checkout's directory when the cwd is in a linked worktree nested inside it (e.g.
|
|
5671
|
+
* `repo/.claude/worktrees/x`), so its same-named file is not applied twice. Sibling worktrees,
|
|
5672
|
+
* bare layouts and submodules are never shadowed.
|
|
5673
|
+
*/
|
|
5674
|
+
const shadowing = (location) => {
|
|
5675
|
+
const store = location.project.vcs?.store;
|
|
5676
|
+
if (location.space.kind !== "linked" || store === void 0 || posix.basename(store) !== ".git") return void 0;
|
|
5677
|
+
const main = posix.dirname(store);
|
|
5678
|
+
return location.space.location.startsWith(`${main}/`) ? main : void 0;
|
|
5679
|
+
};
|
|
5680
|
+
/** One file per directory from the sandbox root down to the cwd, closest last so it has the final word. */
|
|
5681
|
+
const discover = Effect.fnUntraced(function* (fs, location) {
|
|
5682
|
+
const chain = ancestors(location.directory);
|
|
5683
|
+
const files = yield* present$1(fs, chain.flatMap((directory) => names.map((name) => posix.join(directory, name))));
|
|
5684
|
+
const found = yield* Effect.forEach(chain, (directory) => first(fs, directory, files), { concurrency: "unbounded" });
|
|
5685
|
+
const main = shadowing(location);
|
|
5686
|
+
const worktree = main === void 0 ? void 0 : found[chain.indexOf(location.space.location)];
|
|
5687
|
+
const walked = [];
|
|
5688
|
+
for (const [index, here] of found.entries()) {
|
|
5689
|
+
const shadowed = chain[index] === main && worktree !== void 0 && here?.name === worktree.name;
|
|
5690
|
+
if (here !== void 0 && !shadowed) walked.push(here);
|
|
5691
|
+
}
|
|
5692
|
+
return walked.reverse();
|
|
5693
|
+
});
|
|
5694
|
+
const instructionPlugin = (0, plugin_exports$1.define)({
|
|
5695
|
+
id: "codework.prompt.instruction",
|
|
5696
|
+
kind: "prompt",
|
|
5697
|
+
setup: Effect.fn("InstructionPlugin.setup")(function* (ctx) {
|
|
5698
|
+
const fs = yield* io_exports.SandboxIO.FileSystem;
|
|
5699
|
+
const files = (yield* discover(fs, ctx.location)).filter((file) => file.content !== void 0);
|
|
5700
|
+
if (files.length === 0) return;
|
|
5701
|
+
const prompt = ctx.plugin.prompt;
|
|
5702
|
+
prompt.sections.append(Section.ProjectContext, "Project-Specific Instructions & Guidelines:");
|
|
5703
|
+
for (const file of files) prompt.sections.append(Section.ProjectContext, `<project_instructions path="${attribute(file.path)}">\n${file.content}\n</project_instructions>`);
|
|
5073
5704
|
})
|
|
5074
5705
|
});
|
|
5075
5706
|
//#endregion
|
|
@@ -5086,6 +5717,17 @@ const defaultPromptPlugin = (0, plugin_exports$1.define)({
|
|
|
5086
5717
|
*/
|
|
5087
5718
|
const DEFAULT_MAX_LINES = 2e3;
|
|
5088
5719
|
const DEFAULT_MAX_BYTES = 51200;
|
|
5720
|
+
/** Cut one line to `maxChars`, marking the cut. */
|
|
5721
|
+
function truncateLine(line, maxChars = 500) {
|
|
5722
|
+
if (line.length <= maxChars) return {
|
|
5723
|
+
text: line,
|
|
5724
|
+
wasTruncated: false
|
|
5725
|
+
};
|
|
5726
|
+
return {
|
|
5727
|
+
text: `${line.slice(0, maxChars)}... [truncated]`,
|
|
5728
|
+
wasTruncated: true
|
|
5729
|
+
};
|
|
5730
|
+
}
|
|
5089
5731
|
function splitLinesForCounting(content) {
|
|
5090
5732
|
if (content.length === 0) return [];
|
|
5091
5733
|
const lines = content.split("\n");
|
|
@@ -5099,6 +5741,74 @@ function formatSize(bytes) {
|
|
|
5099
5741
|
return `${(bytes / 1048576).toFixed(1)}MB`;
|
|
5100
5742
|
}
|
|
5101
5743
|
/**
|
|
5744
|
+
* Truncate content from the head (keep the first N lines/bytes). Suitable for
|
|
5745
|
+
* file reads where the beginning matters. Never returns partial lines; if the
|
|
5746
|
+
* first line alone exceeds the byte limit, returns empty content with
|
|
5747
|
+
* `firstLineExceedsLimit = true`.
|
|
5748
|
+
*/
|
|
5749
|
+
function truncateHead(content, options = {}) {
|
|
5750
|
+
const maxLines = options.maxLines ?? 2e3;
|
|
5751
|
+
const maxBytes = options.maxBytes ?? 51200;
|
|
5752
|
+
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
5753
|
+
const lines = splitLinesForCounting(content);
|
|
5754
|
+
const totalLines = lines.length;
|
|
5755
|
+
if (totalLines <= maxLines && totalBytes <= maxBytes) return {
|
|
5756
|
+
content,
|
|
5757
|
+
truncated: false,
|
|
5758
|
+
truncatedBy: null,
|
|
5759
|
+
totalLines,
|
|
5760
|
+
totalBytes,
|
|
5761
|
+
outputLines: totalLines,
|
|
5762
|
+
outputBytes: totalBytes,
|
|
5763
|
+
lastLinePartial: false,
|
|
5764
|
+
firstLineExceedsLimit: false,
|
|
5765
|
+
maxLines,
|
|
5766
|
+
maxBytes
|
|
5767
|
+
};
|
|
5768
|
+
if (Buffer.byteLength(lines[0] ?? "", "utf-8") > maxBytes) return {
|
|
5769
|
+
content: "",
|
|
5770
|
+
truncated: true,
|
|
5771
|
+
truncatedBy: "bytes",
|
|
5772
|
+
totalLines,
|
|
5773
|
+
totalBytes,
|
|
5774
|
+
outputLines: 0,
|
|
5775
|
+
outputBytes: 0,
|
|
5776
|
+
lastLinePartial: false,
|
|
5777
|
+
firstLineExceedsLimit: true,
|
|
5778
|
+
maxLines,
|
|
5779
|
+
maxBytes
|
|
5780
|
+
};
|
|
5781
|
+
const outputLinesArr = [];
|
|
5782
|
+
let outputBytesCount = 0;
|
|
5783
|
+
let truncatedBy = "lines";
|
|
5784
|
+
for (let i = 0; i < lines.length && i < maxLines; i++) {
|
|
5785
|
+
const line = lines[i] ?? "";
|
|
5786
|
+
const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0);
|
|
5787
|
+
if (outputBytesCount + lineBytes > maxBytes) {
|
|
5788
|
+
truncatedBy = "bytes";
|
|
5789
|
+
break;
|
|
5790
|
+
}
|
|
5791
|
+
outputLinesArr.push(line);
|
|
5792
|
+
outputBytesCount += lineBytes;
|
|
5793
|
+
}
|
|
5794
|
+
if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) truncatedBy = "lines";
|
|
5795
|
+
const outputContent = outputLinesArr.join("\n");
|
|
5796
|
+
const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
|
|
5797
|
+
return {
|
|
5798
|
+
content: outputContent,
|
|
5799
|
+
truncated: true,
|
|
5800
|
+
truncatedBy,
|
|
5801
|
+
totalLines,
|
|
5802
|
+
totalBytes,
|
|
5803
|
+
outputLines: outputLinesArr.length,
|
|
5804
|
+
outputBytes: finalOutputBytes,
|
|
5805
|
+
lastLinePartial: false,
|
|
5806
|
+
firstLineExceedsLimit: false,
|
|
5807
|
+
maxLines,
|
|
5808
|
+
maxBytes
|
|
5809
|
+
};
|
|
5810
|
+
}
|
|
5811
|
+
/**
|
|
5102
5812
|
* Truncate content from the tail (keep the last N lines/bytes). Suitable for
|
|
5103
5813
|
* bash output where the end matters (errors, final results). May return a
|
|
5104
5814
|
* partial first line if the last line of the original content exceeds the byte
|
|
@@ -5173,9 +5883,6 @@ function truncateStringToBytesFromEnd(str, maxBytes) {
|
|
|
5173
5883
|
}
|
|
5174
5884
|
//#endregion
|
|
5175
5885
|
//#region src/tool/accumulator.ts
|
|
5176
|
-
function defaultTempFilePath(prefix, suffix) {
|
|
5177
|
-
return posix.join(tmpdir(), `${prefix}-${suffix}.log`);
|
|
5178
|
-
}
|
|
5179
5886
|
function byteLength(text) {
|
|
5180
5887
|
return Buffer.byteLength(text, "utf-8");
|
|
5181
5888
|
}
|
|
@@ -5183,9 +5890,7 @@ var Accumulator = class {
|
|
|
5183
5890
|
maxLines;
|
|
5184
5891
|
maxBytes;
|
|
5185
5892
|
maxRollingBytes;
|
|
5186
|
-
tempFilePrefix;
|
|
5187
5893
|
decoder = new TextDecoder();
|
|
5188
|
-
rawChunks = [];
|
|
5189
5894
|
tailText = "";
|
|
5190
5895
|
tailBytes = 0;
|
|
5191
5896
|
tailStartsAtLineBoundary = true;
|
|
@@ -5196,45 +5901,43 @@ var Accumulator = class {
|
|
|
5196
5901
|
currentLineBytes = 0;
|
|
5197
5902
|
hasOpenLine = false;
|
|
5198
5903
|
finished = false;
|
|
5199
|
-
tempFilePath;
|
|
5200
|
-
tempFile;
|
|
5201
5904
|
constructor(options = {}) {
|
|
5202
5905
|
this.maxLines = options.maxLines ?? 2e3;
|
|
5203
5906
|
this.maxBytes = options.maxBytes ?? 51200;
|
|
5204
5907
|
this.maxRollingBytes = Math.max(this.maxBytes * 2, 1);
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
append(data) {
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5908
|
+
}
|
|
5909
|
+
/** `rawBytes` is what `data` measured before a backend decoded it, when that differs. */
|
|
5910
|
+
append(data, rawBytes = data.length) {
|
|
5911
|
+
if (this.finished) throw new Error("cannot append to a finished output accumulator");
|
|
5912
|
+
this.totalRawBytes += rawBytes;
|
|
5913
|
+
this.appendDecodedText(this.decoder.decode(data, { stream: true }));
|
|
5914
|
+
}
|
|
5915
|
+
/**
|
|
5916
|
+
* Count output that was produced but not delivered — the start of the output,
|
|
5917
|
+
* before anything is appended. Unless it is known to end a line, the first
|
|
5918
|
+
* appended line is treated as partial and left out of snapshots.
|
|
5919
|
+
*/
|
|
5920
|
+
skip(bytes, newlines, endsLine) {
|
|
5921
|
+
if (bytes <= 0) return;
|
|
5922
|
+
this.totalRawBytes += bytes;
|
|
5923
|
+
this.totalDecodedBytes += bytes;
|
|
5924
|
+
this.completedLines += newlines;
|
|
5925
|
+
this.totalLines = this.completedLines;
|
|
5926
|
+
this.tailStartsAtLineBoundary = endsLine;
|
|
5219
5927
|
}
|
|
5220
5928
|
finish() {
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
this.appendDecodedText(this.decoder.decode());
|
|
5225
|
-
return Effect.gen({ self: this }, function* () {
|
|
5226
|
-
if (this.shouldUseTempFile()) yield* this.ensureTempFile();
|
|
5227
|
-
if (this.tempFile !== void 0) yield* this.tempFile.sync.pipe(Effect.orDie);
|
|
5228
|
-
});
|
|
5229
|
-
});
|
|
5929
|
+
if (this.finished) return;
|
|
5930
|
+
this.finished = true;
|
|
5931
|
+
this.appendDecodedText(this.decoder.decode());
|
|
5230
5932
|
}
|
|
5231
5933
|
snapshot() {
|
|
5232
5934
|
const tailTruncation = truncateTail(this.getSnapshotText(), {
|
|
5233
5935
|
maxLines: this.maxLines,
|
|
5234
5936
|
maxBytes: this.maxBytes
|
|
5235
5937
|
});
|
|
5236
|
-
const
|
|
5237
|
-
const
|
|
5938
|
+
const beyondLimits = this.totalLines > this.maxLines || this.totalRawBytes > this.maxBytes;
|
|
5939
|
+
const truncated = beyondLimits || tailTruncation.truncated;
|
|
5940
|
+
const truncatedBy = truncated ? tailTruncation.truncatedBy ?? (this.totalRawBytes > this.maxBytes ? "bytes" : "lines") : null;
|
|
5238
5941
|
const truncation = {
|
|
5239
5942
|
...tailTruncation,
|
|
5240
5943
|
truncated,
|
|
@@ -5247,7 +5950,7 @@ var Accumulator = class {
|
|
|
5247
5950
|
return {
|
|
5248
5951
|
content: truncation.content,
|
|
5249
5952
|
truncation,
|
|
5250
|
-
|
|
5953
|
+
beyondLimits
|
|
5251
5954
|
};
|
|
5252
5955
|
}
|
|
5253
5956
|
/** Bytes in the final (possibly unterminated) line — for the partial-line footer. */
|
|
@@ -5293,23 +5996,111 @@ var Accumulator = class {
|
|
|
5293
5996
|
getSnapshotText() {
|
|
5294
5997
|
if (this.tailStartsAtLineBoundary) return this.tailText;
|
|
5295
5998
|
const firstNewline = this.tailText.indexOf("\n");
|
|
5296
|
-
return firstNewline === -1 ? this.tailText : this.tailText.slice(firstNewline + 1);
|
|
5297
|
-
}
|
|
5298
|
-
shouldUseTempFile() {
|
|
5299
|
-
return this.totalRawBytes > this.maxBytes || this.totalDecodedBytes > this.maxBytes || this.totalLines > this.maxLines;
|
|
5300
|
-
}
|
|
5301
|
-
ensureTempFile() {
|
|
5302
|
-
if (this.tempFile !== void 0) return Effect.void;
|
|
5303
|
-
return Effect.gen({ self: this }, function* () {
|
|
5304
|
-
const suffix = Encoding.encodeHex(yield* crypto.randomBytes(8));
|
|
5305
|
-
this.tempFilePath = defaultTempFilePath(this.tempFilePrefix, suffix);
|
|
5306
|
-
this.tempFile = yield* fileSystem.open(this.tempFilePath, { flag: "w" });
|
|
5307
|
-
for (const chunk of this.rawChunks) yield* this.tempFile.writeAll(chunk);
|
|
5308
|
-
this.rawChunks = [];
|
|
5309
|
-
}).pipe(Effect.orDie);
|
|
5999
|
+
return firstNewline === -1 || firstNewline === this.tailText.length - 1 ? this.tailText : this.tailText.slice(firstNewline + 1);
|
|
5310
6000
|
}
|
|
5311
6001
|
};
|
|
5312
6002
|
//#endregion
|
|
6003
|
+
//#region src/tool/output.ts
|
|
6004
|
+
/** A new, unused output file path under `spillPath`. */
|
|
6005
|
+
const path$1 = (spillPath, prefix) => crypto.randomBytes(8).pipe(Effect.map((bytes) => posix.join(spillPath, `${prefix}-${Hex.encode(bytes)}.log`)), Effect.orDie);
|
|
6006
|
+
/** First line printed when the output file cannot be created; the command then runs unsaved. */
|
|
6007
|
+
const unsaved = (file) => `codework: output not saved, cannot create ${file}`;
|
|
6008
|
+
const HEADER = "codework-output";
|
|
6009
|
+
/** Twice the byte limit leaves room to find where the shown lines start. */
|
|
6010
|
+
const windowBytes = (limits) => limits.maxBytes * 2;
|
|
6011
|
+
/**
|
|
6012
|
+
* The wrapper. The command's combined output goes through `pipeline` into the
|
|
6013
|
+
* file; the file's size then decides whether it is kept, and `report` lines run
|
|
6014
|
+
* before an unkept file is removed.
|
|
6015
|
+
*/
|
|
6016
|
+
const script = (command, file, limits, pipeline, report) => [
|
|
6017
|
+
`f=${(0, shell_exports.quote)(file)}`,
|
|
6018
|
+
"run() (",
|
|
6019
|
+
command,
|
|
6020
|
+
")",
|
|
6021
|
+
`if mkdir -p ${(0, shell_exports.quote)(posix.dirname(file))} 2>/dev/null && (umask 077 2>/dev/null; set -C && : > "$f" && : > "$f.rc" && : > "$f.pid") 2>/dev/null; then`,
|
|
6022
|
+
" echo \"$$ $(ps -o pgid= -p $$ 2>/dev/null)\" > \"$f.pid\"",
|
|
6023
|
+
`\t${pipeline("{ run 2>&1; echo $? > \"$f.rc\"; }")}`,
|
|
6024
|
+
" rc=$(cat \"$f.rc\" 2>/dev/null); rm -f \"$f.rc\"",
|
|
6025
|
+
`\tb=$(wc -c "$f" | awk '{ print $1 }'); n=$(wc -l "$f" | awk '{ print $1 }'); l=$n`,
|
|
6026
|
+
`\t[ -s "$f" ] && [ "$(tail -c 1 "$f" | wc -l | awk '{ print $1 }')" = 0 ] && l=$((l + 1))`,
|
|
6027
|
+
`\tkeep=0; { [ "$b" -gt ${limits.maxBytes} ] || [ "$l" -gt ${limits.maxLines} ]; } && keep=1`,
|
|
6028
|
+
...report.map((line) => `\t${line}`),
|
|
6029
|
+
" [ \"$keep\" = 1 ] || rm -f \"$f\"",
|
|
6030
|
+
" rm -f \"$f.pid\"; exit \"${rc:-1}\"",
|
|
6031
|
+
"fi",
|
|
6032
|
+
"rm -f \"$f\" \"$f.rc\" \"$f.pid\" 2>/dev/null",
|
|
6033
|
+
`printf '%s\\n' ${(0, shell_exports.quote)(unsaved(file))}`,
|
|
6034
|
+
"run 2>&1"
|
|
6035
|
+
].join("\n");
|
|
6036
|
+
const header = new RegExp(`^${HEADER} (\\d+) (\\d+) ([01])(?: (\\d+))?$`, "m");
|
|
6037
|
+
const parse$3 = (match) => ({
|
|
6038
|
+
bytes: Number(match[1]),
|
|
6039
|
+
newlines: Number(match[2]),
|
|
6040
|
+
kept: match[3] === "1"
|
|
6041
|
+
});
|
|
6042
|
+
/**
|
|
6043
|
+
* For a backend that streams: all output still flows back as it is produced, and
|
|
6044
|
+
* a copy lands in the file. The report rides on stderr, the wrapper's own channel
|
|
6045
|
+
* since the command's is folded into stdout.
|
|
6046
|
+
*/
|
|
6047
|
+
const streaming = (command, file, limits) => script(command, file, limits, (source) => `${source} | tee -a "$f"`, [`printf '${HEADER} %s %s %s\\n' "$b" "$n" "$keep" >&2`]);
|
|
6048
|
+
/** Read {@link streaming}'s stderr: the report, if it got that far, and anything else the wrapper said. */
|
|
6049
|
+
const report = (stderr) => {
|
|
6050
|
+
const match = header.exec(stderr);
|
|
6051
|
+
if (match === null) return {
|
|
6052
|
+
report: void 0,
|
|
6053
|
+
rest: stderr
|
|
6054
|
+
};
|
|
6055
|
+
return {
|
|
6056
|
+
report: parse$3(match),
|
|
6057
|
+
rest: stderr.slice(0, match.index) + stderr.slice(match.index + match[0].length).replace(/^\n/, "")
|
|
6058
|
+
};
|
|
6059
|
+
};
|
|
6060
|
+
/**
|
|
6061
|
+
* For a backend that returns output only at the end: the output goes to the
|
|
6062
|
+
* file, and only a header — the report plus the window's raw size — and a tail
|
|
6063
|
+
* window come back. The bytes before the window are skipped, and counted.
|
|
6064
|
+
*/
|
|
6065
|
+
const buffered = (command, file, limits) => {
|
|
6066
|
+
const tail = `tail -n ${limits.maxLines} "$f" | tail -c ${windowBytes(limits)}`;
|
|
6067
|
+
return script(command, file, limits, (source) => `{ ${source} | cat; } >> "$f"`, [
|
|
6068
|
+
`w=$(${tail} | wc -c | awk '{ print $1 }')`,
|
|
6069
|
+
`printf '${HEADER} %s %s %s %s\\n' "$b" "$n" "$keep" "$w"`,
|
|
6070
|
+
tail
|
|
6071
|
+
]);
|
|
6072
|
+
};
|
|
6073
|
+
/** Read {@link buffered}'s result; `undefined` when the output was not saved and came back whole. */
|
|
6074
|
+
const window = (stdout) => {
|
|
6075
|
+
const newline = stdout.indexOf("\n");
|
|
6076
|
+
const match = header.exec(newline === -1 ? stdout : stdout.slice(0, newline));
|
|
6077
|
+
if (match === null || match[4] === void 0) return void 0;
|
|
6078
|
+
return {
|
|
6079
|
+
...parse$3(match),
|
|
6080
|
+
text: stdout.slice(newline + 1),
|
|
6081
|
+
textBytes: Number(match[4])
|
|
6082
|
+
};
|
|
6083
|
+
};
|
|
6084
|
+
/**
|
|
6085
|
+
* After a deadline or an interrupt the wrapper never reaches its own cleanup.
|
|
6086
|
+
* Kill what it started — its process group when it led one, else its process
|
|
6087
|
+
* tree, whole while the wrapper itself still runs — and remove the side files,
|
|
6088
|
+
* and the output file unless `keep`.
|
|
6089
|
+
*/
|
|
6090
|
+
const cleanup = (file, keep) => [
|
|
6091
|
+
`f=${(0, shell_exports.quote)(file)}`,
|
|
6092
|
+
"set -- $(cat \"$f.pid\" 2>/dev/null)",
|
|
6093
|
+
`tree() { echo "$1"; for c in $(ps -A -o pid= -o ppid= 2>/dev/null | awk -v p="$1" '$2 == p { print $1 }'); do tree "$c"; done; }`,
|
|
6094
|
+
"if [ -n \"${1:-}\" ]; then",
|
|
6095
|
+
" pids=$(tree \"$1\")",
|
|
6096
|
+
" [ \"${2:-$1}\" = \"$1\" ] && kill -s KILL -- \"-$1\" 2>/dev/null",
|
|
6097
|
+
" kill -9 $pids 2>/dev/null",
|
|
6098
|
+
"fi",
|
|
6099
|
+
`rm -f -- ${keep ? "\"$f.rc\" \"$f.pid\"" : "\"$f\" \"$f.rc\" \"$f.pid\""}`
|
|
6100
|
+
].join("\n");
|
|
6101
|
+
/** Remove an output file that {@link cleanup} kept. */
|
|
6102
|
+
const remove$1 = (file) => `rm -f -- ${(0, shell_exports.quote)(file)}`;
|
|
6103
|
+
//#endregion
|
|
5313
6104
|
//#region src/tool/progress.ts
|
|
5314
6105
|
var progress_exports = /* @__PURE__ */ __exportAll({});
|
|
5315
6106
|
import * as import__codeworksh_plugin_tool_progress from "@codeworksh/plugin/tool/progress";
|
|
@@ -5319,16 +6110,11 @@ __reExport(progress_exports, import__codeworksh_plugin_tool_progress);
|
|
|
5319
6110
|
/**
|
|
5320
6111
|
* `ToolShell` — the tool-owned, cancellable command-execution capability the bash tool depends on.
|
|
5321
6112
|
*
|
|
5322
|
-
* It deliberately does **not** reuse `sandbox/Shell` directly:
|
|
5323
|
-
*
|
|
5324
|
-
*
|
|
5325
|
-
* mid-flight
|
|
5326
|
-
*
|
|
5327
|
-
*
|
|
5328
|
-
* Two modes, and Local has two variants:
|
|
5329
|
-
* - Local / full OS shell → {@link local} (default)
|
|
5330
|
-
* - Local / just-bash → {@link fromSandboxShell} over `EnvBash`
|
|
5331
|
-
* - Remote / provider → {@link fromSandboxShell} over `EnvDaytona` / `EnvVercel`
|
|
6113
|
+
* It deliberately does **not** reuse `sandbox/Shell` directly: `sandbox/Shell`
|
|
6114
|
+
* models no timeout, and `ToolShell` makes it part of the contract. Every
|
|
6115
|
+
* backend reaches it through {@link fromSandboxShell}; the process dies
|
|
6116
|
+
* mid-flight where the backend can kill it (host, Vercel) and the call returns
|
|
6117
|
+
* at the deadline either way.
|
|
5332
6118
|
*/
|
|
5333
6119
|
/** Infra/spawn failure — not model-actionable; handlers should treat it as a defect. */
|
|
5334
6120
|
var ToolShellError = class extends Schema.TaggedError()("ToolShellError", {
|
|
@@ -5383,6 +6169,7 @@ const fromSandboxShell = Layer.effect(ToolShell, Effect.gen(function* () {
|
|
|
5383
6169
|
exitCode: chunk.exitCode
|
|
5384
6170
|
} : {
|
|
5385
6171
|
_tag: "Output",
|
|
6172
|
+
channel: chunk._tag,
|
|
5386
6173
|
bytes: chunk.bytes
|
|
5387
6174
|
}), Stream.mapError((cause) => new ToolShellError({
|
|
5388
6175
|
command,
|
|
@@ -5393,7 +6180,6 @@ const fromSandboxShell = Layer.effect(ToolShell, Effect.gen(function* () {
|
|
|
5393
6180
|
stream
|
|
5394
6181
|
} : { exec });
|
|
5395
6182
|
}));
|
|
5396
|
-
Duration.seconds(1);
|
|
5397
6183
|
//#endregion
|
|
5398
6184
|
//#region src/tool/tool.ts
|
|
5399
6185
|
var tool_exports = /* @__PURE__ */ __exportAll({});
|
|
@@ -5403,28 +6189,32 @@ __reExport(tool_exports, import__codeworksh_plugin_tool);
|
|
|
5403
6189
|
//#region src/plugin/builtin/tool/bash.ts
|
|
5404
6190
|
/**
|
|
5405
6191
|
* The self-contained bash plugin — a worked example for tool plugins. The definition is pure data;
|
|
5406
|
-
* the handler depends only on {@link ToolShell} / {@link ToolProgress}
|
|
5407
|
-
* `sandbox/Shell`, so the backend (
|
|
5408
|
-
* swapped by changing the provided Layer with no change to the tool.
|
|
6192
|
+
* the handler depends only on {@link ToolShell} / {@link ToolProgress} and the mount's
|
|
6193
|
+
* {@link SandboxIO.Current}, never `sandbox/Shell`, so the backend (host / just-bash / remote
|
|
6194
|
+
* provider) is swapped by changing the provided Layer with no change to the tool.
|
|
5409
6195
|
*
|
|
5410
|
-
*
|
|
5411
|
-
*
|
|
5412
|
-
*
|
|
5413
|
-
*
|
|
5414
|
-
*
|
|
5415
|
-
* and is reported live via {@link ToolProgress}; on
|
|
5416
|
-
* produced so far is preserved.
|
|
6196
|
+
* The command runs wrapped (`Output`) so its full output is written to a file inside the sandbox,
|
|
6197
|
+
* kept only when it exceeds the display limits. Two paths, picked by capability:
|
|
6198
|
+
* - **variant A (buffered)** otherwise: the sandbox returns only the tail of the output with its
|
|
6199
|
+
* size, so a huge output never crosses to the harness.
|
|
6200
|
+
* - **variant B (streaming)** when the backend offers `ToolShell.stream`: output flows through
|
|
6201
|
+
* an {@link Accumulator} (bounded memory) and is reported live via {@link ToolProgress}; on
|
|
6202
|
+
* timeout the partial output produced so far is preserved.
|
|
5417
6203
|
*
|
|
5418
6204
|
* Success and failure carry the *same* structured shape
|
|
5419
6205
|
* (`output`, `exitCode`, `truncated`, `fullOutputPath`)
|
|
5420
6206
|
* — a non-zero exit is just `exitCode !== 0`, not a different kind of result.
|
|
5421
6207
|
*/
|
|
6208
|
+
const limits = {
|
|
6209
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
6210
|
+
maxBytes: DEFAULT_MAX_BYTES
|
|
6211
|
+
};
|
|
5422
6212
|
const BashParams = Schema.Struct({
|
|
5423
6213
|
command: Schema.String.annotate({ description: "The bash command to execute." }),
|
|
5424
6214
|
timeout: Schema.optional(Schema.Finite.check(Schema.isGreaterThan(0)).annotate({ description: "Optional timeout in seconds (must be greater than 0)." }))
|
|
5425
6215
|
});
|
|
5426
6216
|
const outputFields = {
|
|
5427
|
-
/** Combined stdout + stderr, truncated for display (full output is
|
|
6217
|
+
/** Combined stdout + stderr, truncated for display (full output is in the sandbox when `truncated`). */
|
|
5428
6218
|
output: Schema.String,
|
|
5429
6219
|
truncated: Schema.Boolean,
|
|
5430
6220
|
fullOutputPath: Schema.optional(Schema.String)
|
|
@@ -5444,175 +6234,1574 @@ var BashTimedOut = class extends Schema.TaggedError()("BashTimedOut", {
|
|
|
5444
6234
|
timeoutSeconds: Schema.Finite
|
|
5445
6235
|
}) {};
|
|
5446
6236
|
const BashFailure = Schema.Union([BashFailed, BashTimedOut]);
|
|
6237
|
+
/** The reason for a failure, after any output: the model sees nothing else of `details`. */
|
|
6238
|
+
const withStatus = (failure) => {
|
|
6239
|
+
const status = failure._tag === "BashTimedOut" ? `Command timed out after ${failure.timeoutSeconds} seconds` : failure.exitCode === -1 ? "Command terminated without an exit code" : `Command exited with code ${failure.exitCode}`;
|
|
6240
|
+
return failure.output ? `${failure.output.replace(/\n$/, "")}\n\n${status}` : status;
|
|
6241
|
+
};
|
|
5447
6242
|
const bashDef = tool_exports.define({
|
|
5448
6243
|
name: "bash",
|
|
5449
6244
|
label: "bash",
|
|
5450
6245
|
promptSnippet: "Execute bash commands (ls, grep, find, etc.).",
|
|
5451
|
-
description: `Execute a bash command in the working directory and return its combined stdout/stderr output. Output is truncated to the last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES}
|
|
6246
|
+
description: `Execute a bash command in the working directory and return its combined stdout/stderr output. Output is truncated to the last ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)} (whichever is hit first); when truncated, a note says which lines are shown and gives the path of a file holding the full output when one was saved. A non-zero exit code is reported as an error carrying the captured output.`,
|
|
5452
6247
|
parameters: BashParams,
|
|
6248
|
+
constrainedSampling: {
|
|
6249
|
+
type: "json_schema",
|
|
6250
|
+
strict: "prefer"
|
|
6251
|
+
},
|
|
5453
6252
|
success: BashSuccess,
|
|
5454
6253
|
failure: BashFailure,
|
|
5455
6254
|
encodeContent: (success) => [{
|
|
5456
6255
|
type: "text",
|
|
5457
|
-
text: success.output
|
|
6256
|
+
text: success.output || "(no output)"
|
|
6257
|
+
}],
|
|
6258
|
+
encodeFailureContent: (failure) => [{
|
|
6259
|
+
type: "text",
|
|
6260
|
+
text: withStatus(failure)
|
|
6261
|
+
}]
|
|
6262
|
+
});
|
|
6263
|
+
/** Combine stdout and stderr into one stream of text, stderr after stdout. */
|
|
6264
|
+
const combineOutput = (stdout, stderr) => {
|
|
6265
|
+
if (stderr.length === 0) return stdout;
|
|
6266
|
+
if (stdout.length === 0) return stderr;
|
|
6267
|
+
return `${stdout}\n${stderr}`;
|
|
6268
|
+
};
|
|
6269
|
+
/** One-line footer appended to truncated, model-facing output. */
|
|
6270
|
+
const footer = (t, fullOutputPath, lastLineBytes) => {
|
|
6271
|
+
const where = fullOutputPath ? ` Full output: ${fullOutputPath}` : "";
|
|
6272
|
+
const startLine = t.totalLines - t.outputLines + 1;
|
|
6273
|
+
if (t.lastLinePartial) {
|
|
6274
|
+
const lineSize = lastLineBytes !== void 0 ? ` (line is ${formatSize(lastLineBytes)})` : "";
|
|
6275
|
+
return `\n\n[showing last ${formatSize(t.outputBytes)} of line ${t.totalLines}${lineSize}.${where}]`;
|
|
6276
|
+
}
|
|
6277
|
+
if (t.truncatedBy === "lines") return `\n\n[showing lines ${startLine}-${t.totalLines} of ${t.totalLines}.${where}]`;
|
|
6278
|
+
return `\n\n[showing lines ${startLine}-${t.totalLines} of ${t.totalLines} (${formatSize(t.maxBytes)} limit).${where}]`;
|
|
6279
|
+
};
|
|
6280
|
+
/** Present a snapshot; `saved` is the output file, when the sandbox kept one. */
|
|
6281
|
+
const present = (snap, lastLineBytes, saved) => {
|
|
6282
|
+
if (!snap.truncation.truncated) return {
|
|
6283
|
+
output: snap.content,
|
|
6284
|
+
truncated: false
|
|
6285
|
+
};
|
|
6286
|
+
return {
|
|
6287
|
+
output: snap.content.replace(/\n$/, "") + footer(snap.truncation, saved, lastLineBytes),
|
|
6288
|
+
truncated: true,
|
|
6289
|
+
...saved !== void 0 ? { fullOutputPath: saved } : {}
|
|
6290
|
+
};
|
|
6291
|
+
};
|
|
6292
|
+
const utf8 = new TextEncoder();
|
|
6293
|
+
/**
|
|
6294
|
+
* A command cut off by its deadline never reached the wrapper's own cleanup: kill
|
|
6295
|
+
* what is left of it and remove its side files, keeping the output file only when
|
|
6296
|
+
* the result points at it.
|
|
6297
|
+
*/
|
|
6298
|
+
const cleanUp = (shell, file, presented) => sweep(shell, cleanup(file, presented.fullOutputPath !== void 0));
|
|
6299
|
+
/** Best-effort housekeeping script: bounded, and never a failure of the tool. */
|
|
6300
|
+
const sweep = (shell, script) => shell.exec(script, { timeout: Duration.seconds(5) }).pipe(Effect.ignore);
|
|
6301
|
+
/** Variant A — buffered: one `exec` that returns the tail of the output and its size. No live progress. */
|
|
6302
|
+
const runBuffered = (shell, params, file) => Effect.gen(function* () {
|
|
6303
|
+
const result = yield* shell.exec(buffered(params.command, file, limits), params.timeout !== void 0 ? { timeout: Duration.seconds(params.timeout) } : void 0).pipe(Effect.catchTags({
|
|
6304
|
+
ToolShellTimeout: (timeout) => cleanUp(shell, file, {
|
|
6305
|
+
output: "",
|
|
6306
|
+
truncated: false
|
|
6307
|
+
}).pipe(Effect.andThen(Effect.fail(new BashTimedOut({
|
|
6308
|
+
timeoutSeconds: timeout.timeoutMillis / 1e3,
|
|
6309
|
+
output: "",
|
|
6310
|
+
truncated: false
|
|
6311
|
+
})))),
|
|
6312
|
+
ToolShellError: (cause) => Effect.die(cause)
|
|
6313
|
+
}));
|
|
6314
|
+
const acc = new Accumulator(limits);
|
|
6315
|
+
const combined = combineOutput(result.stdout, result.stderr);
|
|
6316
|
+
const window$1 = window(combined);
|
|
6317
|
+
if (window$1 === void 0) acc.append(utf8.encode(combined));
|
|
6318
|
+
else {
|
|
6319
|
+
acc.skip(window$1.bytes - window$1.textBytes, window$1.newlines - countNewlines(window$1.text), window$1.textBytes < windowBytes(limits));
|
|
6320
|
+
acc.append(utf8.encode(window$1.text), window$1.textBytes);
|
|
6321
|
+
}
|
|
6322
|
+
acc.finish();
|
|
6323
|
+
const presented = present(acc.snapshot(), acc.getLastLineBytes(), window$1?.kept === true ? file : void 0);
|
|
6324
|
+
if (result.exitCode !== 0) return yield* new BashFailed({
|
|
6325
|
+
exitCode: result.exitCode,
|
|
6326
|
+
...presented
|
|
6327
|
+
});
|
|
6328
|
+
return {
|
|
6329
|
+
exitCode: result.exitCode,
|
|
6330
|
+
...presented
|
|
6331
|
+
};
|
|
6332
|
+
});
|
|
6333
|
+
const countNewlines = (text) => {
|
|
6334
|
+
let count = 0;
|
|
6335
|
+
for (let index = text.indexOf("\n"); index !== -1; index = text.indexOf("\n", index + 1)) count++;
|
|
6336
|
+
return count;
|
|
6337
|
+
};
|
|
6338
|
+
/** Variant B — streaming: accumulate output, report progress, keep partial output on timeout. */
|
|
6339
|
+
const runStreaming = (shell, stream, params, file) => Effect.gen(function* () {
|
|
6340
|
+
const acc = new Accumulator(limits);
|
|
6341
|
+
const progress = yield* progress_exports.ToolProgress;
|
|
6342
|
+
const exitCode = yield* Ref.make(null);
|
|
6343
|
+
const notice = unsaved(file);
|
|
6344
|
+
let head = "";
|
|
6345
|
+
const control = new TextDecoder();
|
|
6346
|
+
let said = "";
|
|
6347
|
+
const consume = stream(streaming(params.command, file, limits)).pipe(Stream.runForEach((event) => event._tag === "Exit" ? Ref.set(exitCode, event.exitCode) : event.channel === "stderr" ? Effect.sync(() => {
|
|
6348
|
+
said += control.decode(event.bytes, { stream: true });
|
|
6349
|
+
}) : Effect.gen(function* () {
|
|
6350
|
+
if (head.length < notice.length) head += new TextDecoder().decode(event.bytes.slice(0, notice.length));
|
|
6351
|
+
acc.append(event.bytes);
|
|
6352
|
+
yield* progress.report({ content: [{
|
|
6353
|
+
type: "text",
|
|
6354
|
+
text: acc.snapshot().content
|
|
6355
|
+
}] });
|
|
6356
|
+
})), Effect.catchTag("ToolShellError", (cause) => Effect.die(cause)));
|
|
6357
|
+
let timedOutAfter;
|
|
6358
|
+
if (params.timeout !== void 0) {
|
|
6359
|
+
const fiber = yield* Effect.forkChild(consume);
|
|
6360
|
+
const finished = yield* Fiber.await(fiber).pipe(Effect.timeoutOption(Duration.seconds(params.timeout)));
|
|
6361
|
+
if (Option.isSome(finished)) yield* Fiber.join(fiber);
|
|
6362
|
+
else {
|
|
6363
|
+
timedOutAfter = params.timeout;
|
|
6364
|
+
yield* sweep(shell, cleanup(file, true));
|
|
6365
|
+
yield* Fiber.interrupt(fiber);
|
|
6366
|
+
}
|
|
6367
|
+
} else yield* consume;
|
|
6368
|
+
const { report: report$1, rest } = report(said + control.decode());
|
|
6369
|
+
if (rest.length > 0) acc.append(utf8.encode(rest));
|
|
6370
|
+
acc.finish();
|
|
6371
|
+
const snapshot = acc.snapshot();
|
|
6372
|
+
const kept = report$1 !== void 0 ? report$1.kept : snapshot.beyondLimits && !head.startsWith(notice);
|
|
6373
|
+
const presented = present(snapshot, acc.getLastLineBytes(), kept ? file : void 0);
|
|
6374
|
+
if (timedOutAfter !== void 0) {
|
|
6375
|
+
if (!kept) yield* sweep(shell, remove$1(file));
|
|
6376
|
+
return yield* new BashTimedOut({
|
|
6377
|
+
timeoutSeconds: timedOutAfter,
|
|
6378
|
+
...presented
|
|
6379
|
+
});
|
|
6380
|
+
}
|
|
6381
|
+
const code = yield* Ref.get(exitCode);
|
|
6382
|
+
if (code === null || code !== 0) return yield* new BashFailed({
|
|
6383
|
+
exitCode: code ?? -1,
|
|
6384
|
+
...presented
|
|
6385
|
+
});
|
|
6386
|
+
return {
|
|
6387
|
+
exitCode: code,
|
|
6388
|
+
...presented
|
|
6389
|
+
};
|
|
6390
|
+
});
|
|
6391
|
+
const bashHandler = (params) => Effect.gen(function* () {
|
|
6392
|
+
const shell = yield* ToolShell;
|
|
6393
|
+
const file = yield* path$1((yield* io_exports.SandboxIO.Current).spillPath, "codework-bash");
|
|
6394
|
+
return yield* (shell.stream !== void 0 ? runStreaming(shell, shell.stream, params, file) : runBuffered(shell, params, file)).pipe(Effect.onInterrupt(() => cleanUp(shell, file, {
|
|
6395
|
+
output: "",
|
|
6396
|
+
truncated: false
|
|
6397
|
+
})));
|
|
6398
|
+
});
|
|
6399
|
+
/** The bash tool: definition + handler, wired the testable (def/exec split) way. */
|
|
6400
|
+
const bashTool = tool_exports.implement(bashDef, bashHandler);
|
|
6401
|
+
const bashPlugin = (0, plugin_exports$1.define)({
|
|
6402
|
+
id: "codework.tool.bash",
|
|
6403
|
+
kind: "tool",
|
|
6404
|
+
setup: Effect.fn("BashPlugin.setup")(function* (ctx) {
|
|
6405
|
+
const shell = yield* io_exports.SandboxIO.Shell;
|
|
6406
|
+
const current = yield* io_exports.SandboxIO.Current;
|
|
6407
|
+
const mounted = Layer.merge(fromSandboxShell.pipe(Layer.provide(Layer.succeed(io_exports.SandboxIO.Shell, shell))), Layer.succeed(io_exports.SandboxIO.Current, current));
|
|
6408
|
+
ctx.plugin.tools.add(tool_exports.provide(bashTool, mounted));
|
|
6409
|
+
})
|
|
6410
|
+
});
|
|
6411
|
+
//#endregion
|
|
6412
|
+
//#region src/tool/diff.ts
|
|
6413
|
+
const detectLineEnding = (content) => {
|
|
6414
|
+
const crlfIdx = content.indexOf("\r\n");
|
|
6415
|
+
const lfIdx = content.indexOf("\n");
|
|
6416
|
+
if (lfIdx === -1 || crlfIdx === -1) return "\n";
|
|
6417
|
+
return crlfIdx < lfIdx ? "\r\n" : "\n";
|
|
6418
|
+
};
|
|
6419
|
+
const normalizeToLF = (text) => text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
6420
|
+
const restoreLineEndings = (text, ending) => ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
|
|
6421
|
+
/** Strip a UTF-8 BOM if present; the BOM is written back after editing. */
|
|
6422
|
+
const stripBom = (content) => content.startsWith("") ? {
|
|
6423
|
+
bom: "",
|
|
6424
|
+
text: content.slice(1)
|
|
6425
|
+
} : {
|
|
6426
|
+
bom: "",
|
|
6427
|
+
text: content
|
|
6428
|
+
};
|
|
6429
|
+
/**
|
|
6430
|
+
* The form both sides are compared in when the exact text is not found:
|
|
6431
|
+
* NFKC, no trailing whitespace per line, ASCII quotes, dashes and spaces.
|
|
6432
|
+
*/
|
|
6433
|
+
const normalizeForFuzzyMatch = (text) => text.normalize("NFKC").split("\n").map((line) => line.trimEnd()).join("\n").replace(/[‘’‚‛]/g, "'").replace(/[“”„‟]/g, "\"").replace(/[‐‑‒–—―−]/g, "-").replace(/[ - ]/g, " ");
|
|
6434
|
+
const splitLinesWithEndings = (content) => content.match(/[^\n]*\n|[^\n]+/g) ?? [];
|
|
6435
|
+
const getLineSpans = (content) => {
|
|
6436
|
+
let offset = 0;
|
|
6437
|
+
return splitLinesWithEndings(content).map((line) => {
|
|
6438
|
+
const span = {
|
|
6439
|
+
start: offset,
|
|
6440
|
+
end: offset + line.length
|
|
6441
|
+
};
|
|
6442
|
+
offset = span.end;
|
|
6443
|
+
return span;
|
|
6444
|
+
});
|
|
6445
|
+
};
|
|
6446
|
+
const getReplacementLineRange = (lines, replacement) => {
|
|
6447
|
+
const replacementStart = replacement.matchIndex;
|
|
6448
|
+
const replacementEnd = replacement.matchIndex + replacement.matchLength;
|
|
6449
|
+
const startLine = lines.findIndex((line) => replacementStart >= line.start && replacementStart < line.end);
|
|
6450
|
+
if (startLine === -1) throw new Error("Replacement range is outside the base content.");
|
|
6451
|
+
let endLine = startLine;
|
|
6452
|
+
while (endLine < lines.length && lines[endLine].end < replacementEnd) endLine++;
|
|
6453
|
+
if (endLine >= lines.length) throw new Error("Replacement range is outside the base content.");
|
|
6454
|
+
return {
|
|
6455
|
+
startLine,
|
|
6456
|
+
endLine: endLine + 1
|
|
6457
|
+
};
|
|
6458
|
+
};
|
|
6459
|
+
const applyReplacements = (content, replacements, offset = 0) => {
|
|
6460
|
+
let result = content;
|
|
6461
|
+
for (let i = replacements.length - 1; i >= 0; i--) {
|
|
6462
|
+
const replacement = replacements[i];
|
|
6463
|
+
const matchIndex = replacement.matchIndex - offset;
|
|
6464
|
+
result = result.substring(0, matchIndex) + replacement.newText + result.substring(matchIndex + replacement.matchLength);
|
|
6465
|
+
}
|
|
6466
|
+
return result;
|
|
6467
|
+
};
|
|
6468
|
+
/**
|
|
6469
|
+
* Apply replacements matched against `baseContent`, a normalized view of
|
|
6470
|
+
* `originalContent`: the lines a replacement touches are rewritten from the
|
|
6471
|
+
* normalized base, every other line is copied back from the original. The
|
|
6472
|
+
* replacement ranges drive this, so duplicate normalized lines cannot be
|
|
6473
|
+
* aligned to the wrong occurrence.
|
|
6474
|
+
*/
|
|
6475
|
+
const applyReplacementsPreservingUnchangedLines = (originalContent, baseContent, replacements) => {
|
|
6476
|
+
const originalLines = splitLinesWithEndings(originalContent);
|
|
6477
|
+
const baseLines = getLineSpans(baseContent);
|
|
6478
|
+
if (originalLines.length !== baseLines.length) throw new Error("Cannot preserve unchanged lines because the base content has a different line count.");
|
|
6479
|
+
const groups = [];
|
|
6480
|
+
for (const replacement of [...replacements].sort((a, b) => a.matchIndex - b.matchIndex)) {
|
|
6481
|
+
const range = getReplacementLineRange(baseLines, replacement);
|
|
6482
|
+
const current = groups[groups.length - 1];
|
|
6483
|
+
if (current && range.startLine < current.endLine) {
|
|
6484
|
+
current.endLine = Math.max(current.endLine, range.endLine);
|
|
6485
|
+
current.replacements.push(replacement);
|
|
6486
|
+
continue;
|
|
6487
|
+
}
|
|
6488
|
+
groups.push({
|
|
6489
|
+
...range,
|
|
6490
|
+
replacements: [replacement]
|
|
6491
|
+
});
|
|
6492
|
+
}
|
|
6493
|
+
let originalLineIndex = 0;
|
|
6494
|
+
let result = "";
|
|
6495
|
+
for (const group of groups) {
|
|
6496
|
+
result += originalLines.slice(originalLineIndex, group.startLine).join("");
|
|
6497
|
+
const groupStartOffset = baseLines[group.startLine].start;
|
|
6498
|
+
const groupEndOffset = baseLines[group.endLine - 1].end;
|
|
6499
|
+
result += applyReplacements(baseContent.slice(groupStartOffset, groupEndOffset), group.replacements, groupStartOffset);
|
|
6500
|
+
originalLineIndex = group.endLine;
|
|
6501
|
+
}
|
|
6502
|
+
return result + originalLines.slice(originalLineIndex).join("");
|
|
6503
|
+
};
|
|
6504
|
+
/** Find `oldText`, exactly first, then in fuzzy-normalized space. */
|
|
6505
|
+
const fuzzyFindText = (content, oldText) => {
|
|
6506
|
+
const exactIndex = content.indexOf(oldText);
|
|
6507
|
+
if (exactIndex !== -1) return {
|
|
6508
|
+
found: true,
|
|
6509
|
+
index: exactIndex,
|
|
6510
|
+
matchLength: oldText.length,
|
|
6511
|
+
usedFuzzyMatch: false,
|
|
6512
|
+
contentForReplacement: content
|
|
6513
|
+
};
|
|
6514
|
+
const fuzzyContent = normalizeForFuzzyMatch(content);
|
|
6515
|
+
const fuzzyOldText = normalizeForFuzzyMatch(oldText);
|
|
6516
|
+
const fuzzyIndex = fuzzyContent.indexOf(fuzzyOldText);
|
|
6517
|
+
if (fuzzyIndex === -1) return {
|
|
6518
|
+
found: false,
|
|
6519
|
+
index: -1,
|
|
6520
|
+
matchLength: 0,
|
|
6521
|
+
usedFuzzyMatch: false,
|
|
6522
|
+
contentForReplacement: content
|
|
6523
|
+
};
|
|
6524
|
+
return {
|
|
6525
|
+
found: true,
|
|
6526
|
+
index: fuzzyIndex,
|
|
6527
|
+
matchLength: fuzzyOldText.length,
|
|
6528
|
+
usedFuzzyMatch: true,
|
|
6529
|
+
contentForReplacement: fuzzyContent
|
|
6530
|
+
};
|
|
6531
|
+
};
|
|
6532
|
+
const countOccurrences = (content, oldText) => normalizeForFuzzyMatch(content).split(normalizeForFuzzyMatch(oldText)).length - 1;
|
|
6533
|
+
const notFound$1 = (path, editIndex, totalEdits) => totalEdits === 1 ? `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.` : `Could not find edits[${editIndex}] in ${path}. The oldText must match exactly including all whitespace and newlines.`;
|
|
6534
|
+
const duplicate = (path, editIndex, totalEdits, occurrences) => totalEdits === 1 ? `Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.` : `Found ${occurrences} occurrences of edits[${editIndex}] in ${path}. Each oldText must be unique. Please provide more context to make it unique.`;
|
|
6535
|
+
const emptyOldText = (path, editIndex, totalEdits) => totalEdits === 1 ? `oldText must not be empty in ${path}.` : `edits[${editIndex}].oldText must not be empty in ${path}.`;
|
|
6536
|
+
const noChange = (path, totalEdits) => totalEdits === 1 ? `No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.` : `No changes made to ${path}. The replacements produced identical content.`;
|
|
6537
|
+
/**
|
|
6538
|
+
* Apply exact-text replacements to LF-normalized content. Every edit is matched
|
|
6539
|
+
* against the same original, then applied last to first so offsets stay valid.
|
|
6540
|
+
* If any edit needs fuzzy matching, all run in fuzzy space and only the touched
|
|
6541
|
+
* lines are written from it. Throws, with the message the model reads, when an
|
|
6542
|
+
* edit is empty, missing, ambiguous, overlapping, or changes nothing.
|
|
6543
|
+
*/
|
|
6544
|
+
const applyEditsToNormalizedContent = (normalizedContent, edits, path) => {
|
|
6545
|
+
const normalizedEdits = edits.map((edit) => ({
|
|
6546
|
+
oldText: normalizeToLF(edit.oldText),
|
|
6547
|
+
newText: normalizeToLF(edit.newText)
|
|
6548
|
+
}));
|
|
6549
|
+
normalizedEdits.forEach((edit, i) => {
|
|
6550
|
+
if (edit.oldText.length === 0) throw new Error(emptyOldText(path, i, normalizedEdits.length));
|
|
6551
|
+
});
|
|
6552
|
+
const usedFuzzyMatch = normalizedEdits.some((edit) => fuzzyFindText(normalizedContent, edit.oldText).usedFuzzyMatch);
|
|
6553
|
+
const replacementBaseContent = usedFuzzyMatch ? normalizeForFuzzyMatch(normalizedContent) : normalizedContent;
|
|
6554
|
+
const matchedEdits = normalizedEdits.map((edit, i) => {
|
|
6555
|
+
const match = fuzzyFindText(replacementBaseContent, edit.oldText);
|
|
6556
|
+
if (!match.found) throw new Error(notFound$1(path, i, normalizedEdits.length));
|
|
6557
|
+
const occurrences = countOccurrences(replacementBaseContent, edit.oldText);
|
|
6558
|
+
if (occurrences > 1) throw new Error(duplicate(path, i, normalizedEdits.length, occurrences));
|
|
6559
|
+
return {
|
|
6560
|
+
editIndex: i,
|
|
6561
|
+
matchIndex: match.index,
|
|
6562
|
+
matchLength: match.matchLength,
|
|
6563
|
+
newText: edit.newText
|
|
6564
|
+
};
|
|
6565
|
+
});
|
|
6566
|
+
matchedEdits.sort((a, b) => a.matchIndex - b.matchIndex);
|
|
6567
|
+
for (let i = 1; i < matchedEdits.length; i++) {
|
|
6568
|
+
const previous = matchedEdits[i - 1];
|
|
6569
|
+
const current = matchedEdits[i];
|
|
6570
|
+
if (previous.matchIndex + previous.matchLength > current.matchIndex) throw new Error(`edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${path}. Merge them into one edit or target disjoint regions.`);
|
|
6571
|
+
}
|
|
6572
|
+
const newContent = usedFuzzyMatch ? applyReplacementsPreservingUnchangedLines(normalizedContent, replacementBaseContent, matchedEdits) : applyReplacements(replacementBaseContent, matchedEdits);
|
|
6573
|
+
if (normalizedContent === newContent) throw new Error(noChange(path, normalizedEdits.length));
|
|
6574
|
+
return {
|
|
6575
|
+
baseContent: normalizedContent,
|
|
6576
|
+
newContent
|
|
6577
|
+
};
|
|
6578
|
+
};
|
|
6579
|
+
/** A standard unified patch. */
|
|
6580
|
+
const generateUnifiedPatch = (path, oldContent, newContent, contextLines = 4) => Diff.createTwoFilesPatch(path, path, oldContent, newContent, void 0, void 0, {
|
|
6581
|
+
context: contextLines,
|
|
6582
|
+
headerOptions: Diff.FILE_HEADERS_ONLY
|
|
6583
|
+
});
|
|
6584
|
+
/**
|
|
6585
|
+
* A display diff with line numbers and `contextLines` of context around each
|
|
6586
|
+
* change, plus the first changed line in the new file.
|
|
6587
|
+
*/
|
|
6588
|
+
const generateDiffString = (oldContent, newContent, contextLines = 4) => {
|
|
6589
|
+
const parts = Diff.diffLines(oldContent, newContent);
|
|
6590
|
+
const output = [];
|
|
6591
|
+
const lineNumWidth = String(Math.max(oldContent.split("\n").length, newContent.split("\n").length)).length;
|
|
6592
|
+
const numbered = (prefix, lineNum, line) => `${prefix}${String(lineNum).padStart(lineNumWidth, " ")} ${line}`;
|
|
6593
|
+
const ellipsis = ` ${"".padStart(lineNumWidth, " ")} ...`;
|
|
6594
|
+
let oldLineNum = 1;
|
|
6595
|
+
let newLineNum = 1;
|
|
6596
|
+
let lastWasChange = false;
|
|
6597
|
+
let firstChangedLine;
|
|
6598
|
+
parts.forEach((part, i) => {
|
|
6599
|
+
const raw = part.value.split("\n");
|
|
6600
|
+
if (raw[raw.length - 1] === "") raw.pop();
|
|
6601
|
+
if (part.added || part.removed) {
|
|
6602
|
+
firstChangedLine ??= newLineNum;
|
|
6603
|
+
for (const line of raw) if (part.added) output.push(numbered("+", newLineNum++, line));
|
|
6604
|
+
else output.push(numbered("-", oldLineNum++, line));
|
|
6605
|
+
lastWasChange = true;
|
|
6606
|
+
return;
|
|
6607
|
+
}
|
|
6608
|
+
const context = (lines) => {
|
|
6609
|
+
for (const line of lines) {
|
|
6610
|
+
output.push(numbered(" ", oldLineNum, line));
|
|
6611
|
+
oldLineNum++;
|
|
6612
|
+
newLineNum++;
|
|
6613
|
+
}
|
|
6614
|
+
};
|
|
6615
|
+
const skip = (count) => {
|
|
6616
|
+
output.push(ellipsis);
|
|
6617
|
+
oldLineNum += count;
|
|
6618
|
+
newLineNum += count;
|
|
6619
|
+
};
|
|
6620
|
+
const next = parts[i + 1];
|
|
6621
|
+
const hasTrailingChange = next !== void 0 && (next.added || next.removed);
|
|
6622
|
+
if (lastWasChange && hasTrailingChange) {
|
|
6623
|
+
if (raw.length <= contextLines * 2) context(raw);
|
|
6624
|
+
else {
|
|
6625
|
+
context(raw.slice(0, contextLines));
|
|
6626
|
+
skip(raw.length - contextLines * 2);
|
|
6627
|
+
context(raw.slice(raw.length - contextLines));
|
|
6628
|
+
}
|
|
6629
|
+
} else if (lastWasChange) {
|
|
6630
|
+
context(raw.slice(0, contextLines));
|
|
6631
|
+
if (raw.length > contextLines) skip(raw.length - contextLines);
|
|
6632
|
+
} else if (hasTrailingChange) {
|
|
6633
|
+
const skipped = Math.max(0, raw.length - contextLines);
|
|
6634
|
+
if (skipped > 0) skip(skipped);
|
|
6635
|
+
context(raw.slice(skipped));
|
|
6636
|
+
} else {
|
|
6637
|
+
oldLineNum += raw.length;
|
|
6638
|
+
newLineNum += raw.length;
|
|
6639
|
+
}
|
|
6640
|
+
lastWasChange = false;
|
|
6641
|
+
});
|
|
6642
|
+
return {
|
|
6643
|
+
diff: output.join("\n"),
|
|
6644
|
+
firstChangedLine
|
|
6645
|
+
};
|
|
6646
|
+
};
|
|
6647
|
+
//#endregion
|
|
6648
|
+
//#region src/tool/path.ts
|
|
6649
|
+
/**
|
|
6650
|
+
* How file tools read the path a model wrote. Models copy paths out of prose and
|
|
6651
|
+
* terminals, so a path arrives with Unicode spaces or an `@` mention prefix the
|
|
6652
|
+
* file never had. Relative paths stay relative: the mount resolves them.
|
|
6653
|
+
*/
|
|
6654
|
+
const UNICODE_SPACES = /[ - ]/g;
|
|
6655
|
+
const NARROW_NO_BREAK_SPACE = " ";
|
|
6656
|
+
/** The path a file tool acts on. */
|
|
6657
|
+
const normalize$1 = (path) => {
|
|
6658
|
+
const normalized = path.replace(UNICODE_SPACES, " ");
|
|
6659
|
+
return normalized.startsWith("@") ? normalized.slice(1) : normalized;
|
|
6660
|
+
};
|
|
6661
|
+
/**
|
|
6662
|
+
* The path to read: {@link normalize}d, or the first existing spelling the model
|
|
6663
|
+
* could not have typed — macOS screenshot names use a narrow no-break space
|
|
6664
|
+
* before AM/PM, decomposed (NFD) accents, and curly apostrophes. Falls back to
|
|
6665
|
+
* the normalized path, so a missing file is reported under the name asked for.
|
|
6666
|
+
*/
|
|
6667
|
+
const resolveRead = (fs, path) => Effect.gen(function* () {
|
|
6668
|
+
const normalized = normalize$1(path);
|
|
6669
|
+
const variants = /* @__PURE__ */ new Set([
|
|
6670
|
+
normalized,
|
|
6671
|
+
normalized.replace(/ (AM|PM)\./gi, `${NARROW_NO_BREAK_SPACE}$1.`),
|
|
6672
|
+
normalized.normalize("NFD"),
|
|
6673
|
+
normalized.replace(/'/g, "’"),
|
|
6674
|
+
normalized.normalize("NFD").replace(/'/g, "’")
|
|
6675
|
+
]);
|
|
6676
|
+
for (const variant of variants) if (yield* fs.exists(variant)) return variant;
|
|
6677
|
+
return normalized;
|
|
6678
|
+
});
|
|
6679
|
+
//#endregion
|
|
6680
|
+
//#region src/plugin/builtin/tool/edit.ts
|
|
6681
|
+
/**
|
|
6682
|
+
* The built-in edit plugin: exact-text replacements, all matched against the
|
|
6683
|
+
* original file, under the file's mutation lock for the whole read → match →
|
|
6684
|
+
* write. The model reads a one-line summary; `details` carries the diff.
|
|
6685
|
+
*/
|
|
6686
|
+
const ReplaceEdit = Schema.Struct({
|
|
6687
|
+
oldText: Schema.String.annotate({ description: "Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call." }),
|
|
6688
|
+
newText: Schema.String.annotate({ description: "Replacement text for this targeted edit." })
|
|
6689
|
+
});
|
|
6690
|
+
const EditParams = Schema.Struct({
|
|
6691
|
+
path: Schema.String.annotate({ description: "Path to the file to edit (relative or absolute)" }),
|
|
6692
|
+
edits: Schema.Array(ReplaceEdit).annotate({ description: "One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead." })
|
|
6693
|
+
});
|
|
6694
|
+
const EditSuccess = Schema.Struct({
|
|
6695
|
+
path: Schema.String,
|
|
6696
|
+
replaced: Schema.Finite,
|
|
6697
|
+
diff: Schema.String,
|
|
6698
|
+
patch: Schema.String,
|
|
6699
|
+
firstChangedLine: Schema.optional(Schema.Finite)
|
|
6700
|
+
});
|
|
6701
|
+
var EditFailed = class extends Schema.TaggedError()("EditFailed", { message: Schema.String }) {};
|
|
6702
|
+
const isSingleEdit = (value) => {
|
|
6703
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
6704
|
+
const edit = value;
|
|
6705
|
+
return typeof edit.oldText === "string" && typeof edit.newText === "string";
|
|
6706
|
+
};
|
|
6707
|
+
/**
|
|
6708
|
+
* Repair shapes models commonly send: `edits` as a JSON string or as a single
|
|
6709
|
+
* edit object, and a top-level `oldText`/`newText` pair. Works on a copy; the
|
|
6710
|
+
* call's arguments stay unchanged.
|
|
6711
|
+
*/
|
|
6712
|
+
const prepareEditArguments = (input) => {
|
|
6713
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return input;
|
|
6714
|
+
const args = { ...input };
|
|
6715
|
+
if (typeof args.edits === "string") try {
|
|
6716
|
+
const parsed = JSON.parse(args.edits);
|
|
6717
|
+
if (Array.isArray(parsed)) args.edits = parsed;
|
|
6718
|
+
else if (isSingleEdit(parsed)) args.edits = [parsed];
|
|
6719
|
+
} catch {}
|
|
6720
|
+
else if (isSingleEdit(args.edits)) args.edits = [args.edits];
|
|
6721
|
+
if (typeof args.oldText !== "string" || typeof args.newText !== "string") return args;
|
|
6722
|
+
const edits = Array.isArray(args.edits) ? [...args.edits] : [];
|
|
6723
|
+
edits.push({
|
|
6724
|
+
oldText: args.oldText,
|
|
6725
|
+
newText: args.newText
|
|
6726
|
+
});
|
|
6727
|
+
const { oldText: _oldText, newText: _newText, ...rest } = args;
|
|
6728
|
+
return {
|
|
6729
|
+
...rest,
|
|
6730
|
+
edits
|
|
6731
|
+
};
|
|
6732
|
+
};
|
|
6733
|
+
const editDef = tool_exports.define({
|
|
6734
|
+
name: "edit",
|
|
6735
|
+
label: "edit",
|
|
6736
|
+
description: "Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.",
|
|
6737
|
+
promptSnippet: "Make precise file edits with exact text replacement, including multiple disjoint edits in one call",
|
|
6738
|
+
promptGuidelines: [
|
|
6739
|
+
"Use edit for precise changes (edits[].oldText must match exactly)",
|
|
6740
|
+
"When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls",
|
|
6741
|
+
"Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.",
|
|
6742
|
+
"Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions."
|
|
6743
|
+
],
|
|
6744
|
+
prepareArguments: prepareEditArguments,
|
|
6745
|
+
parameters: EditParams,
|
|
6746
|
+
constrainedSampling: {
|
|
6747
|
+
type: "json_schema",
|
|
6748
|
+
strict: "prefer"
|
|
6749
|
+
},
|
|
6750
|
+
success: EditSuccess,
|
|
6751
|
+
failure: EditFailed,
|
|
6752
|
+
encodeContent: (success) => [{
|
|
6753
|
+
type: "text",
|
|
6754
|
+
text: `Successfully replaced ${success.replaced} block(s) in ${success.path}.`
|
|
6755
|
+
}],
|
|
6756
|
+
encodeFailureContent: (failure) => [{
|
|
6757
|
+
type: "text",
|
|
6758
|
+
text: failure.message
|
|
6759
|
+
}],
|
|
6760
|
+
encodeDetails: ({ diff, patch, firstChangedLine }) => ({
|
|
6761
|
+
diff,
|
|
6762
|
+
patch,
|
|
6763
|
+
...firstChangedLine === void 0 ? {} : { firstChangedLine }
|
|
6764
|
+
})
|
|
6765
|
+
});
|
|
6766
|
+
const fail$4 = (message) => Effect.fail(new EditFailed({ message }));
|
|
6767
|
+
/** Filesystem failures name the backend's error code, as Pi's do. */
|
|
6768
|
+
const accessFailure = (path) => (error) => {
|
|
6769
|
+
const code = error.cause?.code;
|
|
6770
|
+
return fail$4(`Could not edit file: ${path}. Error code: ${typeof code === "string" ? code : "unknown"}.`);
|
|
6771
|
+
};
|
|
6772
|
+
const editHandler = (params) => Effect.gen(function* () {
|
|
6773
|
+
const { path, edits } = params;
|
|
6774
|
+
if (edits.length === 0) return yield* fail$4("Edit tool input is invalid. edits must contain at least one replacement.");
|
|
6775
|
+
const fs = yield* io_exports.SandboxIO.FileSystem;
|
|
6776
|
+
const mutation = yield* io_exports.SandboxIO.Mutation;
|
|
6777
|
+
const resolved = normalize$1(path);
|
|
6778
|
+
return yield* mutation.withFile(resolved, Effect.gen(function* () {
|
|
6779
|
+
if (!(yield* fs.stat(resolved)).isFile) return yield* fail$4(`Could not edit file: ${path}. Path is not a file.`);
|
|
6780
|
+
const { bom, text: content } = stripBom(yield* fs.readFile(resolved));
|
|
6781
|
+
const originalEnding = detectLineEnding(content);
|
|
6782
|
+
const { baseContent, newContent } = yield* Effect.try({
|
|
6783
|
+
try: () => applyEditsToNormalizedContent(normalizeToLF(content), edits, path),
|
|
6784
|
+
catch: (cause) => new EditFailed({ message: cause instanceof Error ? cause.message : String(cause) })
|
|
6785
|
+
});
|
|
6786
|
+
yield* fs.writeFile(resolved, bom + restoreLineEndings(newContent, originalEnding));
|
|
6787
|
+
const { diff, firstChangedLine } = generateDiffString(baseContent, newContent);
|
|
6788
|
+
return {
|
|
6789
|
+
path,
|
|
6790
|
+
replaced: edits.length,
|
|
6791
|
+
diff,
|
|
6792
|
+
patch: generateUnifiedPatch(path, baseContent, newContent),
|
|
6793
|
+
...firstChangedLine === void 0 ? {} : { firstChangedLine }
|
|
6794
|
+
};
|
|
6795
|
+
}));
|
|
6796
|
+
}).pipe(Effect.catchTag("SandboxFileSystemError", accessFailure(params.path)));
|
|
6797
|
+
/** The edit tool: definition + handler, wired the testable (def/exec split) way. */
|
|
6798
|
+
const editTool = tool_exports.implement(editDef, editHandler);
|
|
6799
|
+
const editPlugin = (0, plugin_exports$1.define)({
|
|
6800
|
+
id: "codework.tool.edit",
|
|
6801
|
+
kind: "tool",
|
|
6802
|
+
setup: Effect.fn("EditPlugin.setup")(function* (ctx) {
|
|
6803
|
+
const mounted = Layer.merge(Layer.succeed(io_exports.SandboxIO.FileSystem, yield* io_exports.SandboxIO.FileSystem), Layer.succeed(io_exports.SandboxIO.Mutation, yield* io_exports.SandboxIO.Mutation));
|
|
6804
|
+
ctx.plugin.tools.add(tool_exports.provide(editTool, mounted));
|
|
6805
|
+
})
|
|
6806
|
+
});
|
|
6807
|
+
//#endregion
|
|
6808
|
+
//#region src/tool/binary.ts
|
|
6809
|
+
/** `<home>/bin` on the host, where binaries for `local` mounts are installed. */
|
|
6810
|
+
var HostBin = class extends Context.Service()("@codeworksh/harness/tool/binary/HostBin") {};
|
|
6811
|
+
const specs = {
|
|
6812
|
+
rg: {
|
|
6813
|
+
label: "ripgrep (rg)",
|
|
6814
|
+
repo: "BurntSushi/ripgrep",
|
|
6815
|
+
tagPrefix: "",
|
|
6816
|
+
commands: ["rg"],
|
|
6817
|
+
asset: (version, os, arch) => `ripgrep-${version}-${arch}-${os === "darwin" ? "apple-darwin" : "unknown-linux-musl"}.tar.gz`
|
|
6818
|
+
},
|
|
6819
|
+
fd: {
|
|
6820
|
+
label: "fd",
|
|
6821
|
+
repo: "sharkdp/fd",
|
|
6822
|
+
tagPrefix: "v",
|
|
6823
|
+
commands: ["fd", "fdfind"],
|
|
6824
|
+
asset: (version, os, arch) => `fd-v${version}-${arch}-${os === "darwin" ? "apple-darwin" : "unknown-linux-musl"}.tar.gz`
|
|
6825
|
+
}
|
|
6826
|
+
};
|
|
6827
|
+
const NETWORK_TIMEOUT = Duration.seconds(10);
|
|
6828
|
+
const DOWNLOAD_TIMEOUT = Duration.seconds(120);
|
|
6829
|
+
/** Why a missing binary could not be installed; the model reads the message. */
|
|
6830
|
+
var InstallError$1 = class extends Schema.TaggedError()("InstallError", { message: Schema.String }) {};
|
|
6831
|
+
const installError = (message) => new InstallError$1({ message });
|
|
6832
|
+
const available = /* @__PURE__ */ new Map();
|
|
6833
|
+
const keyOf = (sandbox, name) => `${sandbox.id}\0${name}`;
|
|
6834
|
+
const run$2 = (shell, command) => shell.exec(command).pipe(Effect.orElseSucceed(() => ({
|
|
6835
|
+
stdout: "",
|
|
6836
|
+
stderr: "",
|
|
6837
|
+
exitCode: 127
|
|
6838
|
+
})));
|
|
6839
|
+
const works = (shell, command) => Effect.map(run$2(shell, `${(0, shell_exports.quote)(command)} --version`), (result) => result.exitCode === 0);
|
|
6840
|
+
/** A GET whose `read` of the response, body included, finishes within `timeout`. */
|
|
6841
|
+
const get$2 = (url, timeout, read, init = {}) => HttpClient.get(url).pipe(Effect.flatMap(read), Effect.timeout(timeout), Effect.mapError((cause) => Schema.is(InstallError$1)(cause) ? cause : installError(`${url}: ${cause instanceof Error ? cause.message : String(cause)}`)), Effect.provide(FetchHttpClient.layer), Effect.provideService(FetchHttpClient.RequestInit, init));
|
|
6842
|
+
/** The latest release, read from the redirect of the release page; the API endpoint is rate limited. */
|
|
6843
|
+
const latestVersion = (repo) => Effect.gen(function* () {
|
|
6844
|
+
const response = yield* get$2(`https://github.com/${repo}/releases/latest`, NETWORK_TIMEOUT, Effect.succeed, { redirect: "manual" });
|
|
6845
|
+
const location = response.headers["location"] ?? "";
|
|
6846
|
+
const tag = new URL(location, "https://github.com").pathname.split("/").pop();
|
|
6847
|
+
if (!location.includes("/releases/tag/") || !tag) return yield* installError(`Failed to resolve latest ${repo} release: HTTP ${response.status}`);
|
|
6848
|
+
return decodeURIComponent(tag).replace(/^v/, "");
|
|
6849
|
+
});
|
|
6850
|
+
const platformOf = (shell) => Effect.gen(function* () {
|
|
6851
|
+
const [os, arch] = (yield* run$2(shell, "uname -sm")).stdout.trim().toLowerCase().split(/\s+/);
|
|
6852
|
+
const mappedOs = os === "darwin" || os === "linux" ? os : void 0;
|
|
6853
|
+
const mappedArch = arch === "x86_64" || arch === "amd64" ? "x86_64" : arch === "aarch64" || arch === "arm64" ? "aarch64" : void 0;
|
|
6854
|
+
if (mappedOs === void 0 || mappedArch === void 0) return yield* installError(`Unsupported platform: ${os ?? "unknown"}/${arch ?? "unknown"}`);
|
|
6855
|
+
return {
|
|
6856
|
+
os: mappedOs,
|
|
6857
|
+
arch: mappedArch
|
|
6858
|
+
};
|
|
6859
|
+
});
|
|
6860
|
+
/** Unpacks with the sandbox's own `tar`, as Pi does on its host; the archive layout varies by release. */
|
|
6861
|
+
const unpack = [
|
|
6862
|
+
"set -e",
|
|
6863
|
+
"dir=$1; archive=$2; name=$3; tmp=\"$dir/.extract-$name-$$\"",
|
|
6864
|
+
"trap 'rm -rf \"$tmp\" \"$archive\"' EXIT",
|
|
6865
|
+
"mkdir -p \"$tmp\"",
|
|
6866
|
+
"tar -xzf \"$archive\" -C \"$tmp\"",
|
|
6867
|
+
"found=$(find \"$tmp\" -type f -name \"$name\" | head -n 1)",
|
|
6868
|
+
"[ -n \"$found\" ] || { echo \"binary $name not found in archive\" >&2; exit 1; }",
|
|
6869
|
+
"mv \"$found\" \"$dir/$name\"",
|
|
6870
|
+
"chmod 755 \"$dir/$name\""
|
|
6871
|
+
].join("\n");
|
|
6872
|
+
const install$1 = (name, shell, fs, dir) => Effect.gen(function* () {
|
|
6873
|
+
const spec = specs[name];
|
|
6874
|
+
const { os, arch } = yield* platformOf(shell);
|
|
6875
|
+
const version = name === "fd" && os === "darwin" && arch === "x86_64" ? "10.3.0" : yield* latestVersion(spec.repo);
|
|
6876
|
+
const asset = spec.asset(version, os, arch);
|
|
6877
|
+
const url = `https://github.com/${spec.repo}/releases/download/${spec.tagPrefix}${version}/${asset}`;
|
|
6878
|
+
const bytes = new Uint8Array(yield* get$2(url, DOWNLOAD_TIMEOUT, (response) => response.status === 200 ? response.arrayBuffer : Effect.fail(installError(`Download failed with HTTP ${response.status}: ${url}`))));
|
|
6879
|
+
const archive = posix.join(dir, `.${asset}`);
|
|
6880
|
+
yield* fs.writeFile(archive, bytes).pipe(Effect.mapError((error) => installError(`Cannot write ${archive}: ${error.cause instanceof Error ? error.cause.message : error.message}`)));
|
|
6881
|
+
const result = yield* run$2(shell, `sh -c ${(0, shell_exports.quote)(unpack)} _ ${(0, shell_exports.quoteArgv)([
|
|
6882
|
+
dir,
|
|
6883
|
+
archive,
|
|
6884
|
+
name
|
|
6885
|
+
])}`);
|
|
6886
|
+
if (result.exitCode !== 0) return yield* installError(`Failed to unpack ${asset}: ${(result.stderr || result.stdout).trim()}`);
|
|
6887
|
+
return posix.join(dir, name);
|
|
6888
|
+
});
|
|
6889
|
+
/** Where `kind` keeps installed binaries. */
|
|
6890
|
+
const binDir = (sandbox, shell) => Effect.gen(function* () {
|
|
6891
|
+
if (sandbox.kind === "local") return yield* HostBin;
|
|
6892
|
+
const home = (yield* run$2(shell, "printf %s \"$HOME\"")).stdout.trim();
|
|
6893
|
+
return posix.join(home || "/tmp", ".codework", "bin");
|
|
6894
|
+
});
|
|
6895
|
+
const resolve$3 = (name, sandbox, shell, fs) => Effect.gen(function* () {
|
|
6896
|
+
const dir = yield* binDir(sandbox, shell);
|
|
6897
|
+
for (const command of [posix.join(dir, name), ...specs[name].commands]) if (yield* works(shell, command)) return {
|
|
6898
|
+
_tag: "Available",
|
|
6899
|
+
command
|
|
6900
|
+
};
|
|
6901
|
+
const installed = yield* install$1(name, shell, fs, dir).pipe(Effect.result);
|
|
6902
|
+
if (installed._tag === "Success" && (yield* works(shell, installed.success))) return {
|
|
6903
|
+
_tag: "Available",
|
|
6904
|
+
command: installed.success
|
|
6905
|
+
};
|
|
6906
|
+
const reason = installed._tag === "Failure" ? `: ${installed.failure.message}` : "";
|
|
6907
|
+
return {
|
|
6908
|
+
_tag: "Unavailable",
|
|
6909
|
+
message: `${specs[name].label} is not available in this sandbox and could not be installed${reason}`
|
|
6910
|
+
};
|
|
6911
|
+
});
|
|
6912
|
+
const virtual = (name) => name === "rg" ? {
|
|
6913
|
+
_tag: "Available",
|
|
6914
|
+
command: "rg"
|
|
6915
|
+
} : {
|
|
6916
|
+
_tag: "Unavailable",
|
|
6917
|
+
message: `${specs[name].label} is not available in this sandbox`
|
|
6918
|
+
};
|
|
6919
|
+
/**
|
|
6920
|
+
* The shared probe for `name` in this sandbox, started when none is remembered.
|
|
6921
|
+
* It runs detached, so cancelling the call that started it cannot cancel it for
|
|
6922
|
+
* the others waiting on it; an unavailable result is not remembered, so the next
|
|
6923
|
+
* call tries again.
|
|
6924
|
+
*/
|
|
6925
|
+
const probe$1 = (name, sandbox, shell, fs) => Effect.gen(function* () {
|
|
6926
|
+
const key = keyOf(sandbox, name);
|
|
6927
|
+
const existing = available.get(key);
|
|
6928
|
+
if (existing !== void 0) return existing;
|
|
6929
|
+
const deferred = Deferred.makeUnsafe();
|
|
6930
|
+
available.set(key, deferred);
|
|
6931
|
+
yield* resolve$3(name, sandbox, shell, fs).pipe(Effect.onExit((exit) => Effect.sync(() => {
|
|
6932
|
+
if ((exit._tag === "Failure" || exit.value._tag === "Unavailable") && available.get(key) === deferred) available.delete(key);
|
|
6933
|
+
})), Effect.onExit((exit) => Deferred.done(deferred, exit)), Effect.forkDetach);
|
|
6934
|
+
return deferred;
|
|
6935
|
+
});
|
|
6936
|
+
/**
|
|
6937
|
+
* Run `use` with the command for `name`. When the run reports the binary gone
|
|
6938
|
+
* (`exitCode` 127, e.g. a sandbox reset to a fresh image), the remembered probe is
|
|
6939
|
+
* dropped and the run is tried once more with a fresh one. Only the probe that
|
|
6940
|
+
* gave the stale command is dropped, so concurrent retries share one new probe.
|
|
6941
|
+
*/
|
|
6942
|
+
const withCommand = (name, sandbox, shell, fs, use) => Effect.gen(function* () {
|
|
6943
|
+
if (sandbox.kind === "virtual") {
|
|
6944
|
+
const resolved = virtual(name);
|
|
6945
|
+
return resolved._tag === "Unavailable" ? resolved : yield* use(resolved.command);
|
|
6946
|
+
}
|
|
6947
|
+
for (let attempt = 0;; attempt++) {
|
|
6948
|
+
const deferred = yield* probe$1(name, sandbox, shell, fs);
|
|
6949
|
+
const resolved = yield* Deferred.await(deferred);
|
|
6950
|
+
if (resolved._tag === "Unavailable") return resolved;
|
|
6951
|
+
const result = yield* use(resolved.command);
|
|
6952
|
+
if (result.exitCode !== 127 || attempt > 0) return result;
|
|
6953
|
+
const key = keyOf(sandbox, name);
|
|
6954
|
+
if (available.get(key) === deferred) available.delete(key);
|
|
6955
|
+
}
|
|
6956
|
+
});
|
|
6957
|
+
//#endregion
|
|
6958
|
+
//#region src/plugin/builtin/tool/find.ts
|
|
6959
|
+
/**
|
|
6960
|
+
* The built-in find plugin: fd in the mounted sandbox. A virtual mount has no fd,
|
|
6961
|
+
* and the tool says so rather than emulating it.
|
|
6962
|
+
*/
|
|
6963
|
+
const FindParams = Schema.Struct({
|
|
6964
|
+
pattern: Schema.String.annotate({ description: "Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'" }),
|
|
6965
|
+
path: Schema.optional(Schema.String.annotate({ description: "Directory to search in (default: current directory)" })),
|
|
6966
|
+
limit: Schema.optional(Schema.Finite.annotate({ description: "Maximum number of results (default: 1000)" }))
|
|
6967
|
+
});
|
|
6968
|
+
const FindDetails = Schema.Struct({
|
|
6969
|
+
truncation: Schema.optional(Schema.Unknown),
|
|
6970
|
+
resultLimitReached: Schema.optional(Schema.Finite)
|
|
6971
|
+
});
|
|
6972
|
+
const FindSuccess = Schema.Struct({
|
|
6973
|
+
text: Schema.String,
|
|
6974
|
+
details: Schema.optional(FindDetails)
|
|
6975
|
+
});
|
|
6976
|
+
var FindFailed = class extends Schema.TaggedError()("FindFailed", { message: Schema.String }) {};
|
|
6977
|
+
const DEFAULT_LIMIT$2 = 1e3;
|
|
6978
|
+
const FD_ERROR = "[fd error]:";
|
|
6979
|
+
const findDef = tool_exports.define({
|
|
6980
|
+
name: "find",
|
|
6981
|
+
label: "find",
|
|
6982
|
+
description: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT$2} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,
|
|
6983
|
+
promptSnippet: "Find files by glob pattern (respects .gitignore)",
|
|
6984
|
+
parameters: FindParams,
|
|
6985
|
+
constrainedSampling: {
|
|
6986
|
+
type: "json_schema",
|
|
6987
|
+
strict: "prefer"
|
|
6988
|
+
},
|
|
6989
|
+
success: FindSuccess,
|
|
6990
|
+
failure: FindFailed,
|
|
6991
|
+
encodeContent: (success) => [{
|
|
6992
|
+
type: "text",
|
|
6993
|
+
text: success.text
|
|
6994
|
+
}],
|
|
6995
|
+
encodeFailureContent: (failure) => [{
|
|
6996
|
+
type: "text",
|
|
6997
|
+
text: failure.message
|
|
6998
|
+
}],
|
|
6999
|
+
encodeDetails: (success) => success.details
|
|
7000
|
+
});
|
|
7001
|
+
const fail$3 = (message) => Effect.fail(new FindFailed({ message }));
|
|
7002
|
+
/** Relative to the search root, keeping a directory's trailing slash. */
|
|
7003
|
+
const relativize = (result, searchPath) => {
|
|
7004
|
+
const relative = posix.isAbsolute(result) ? posix.relative(searchPath, result) : result;
|
|
7005
|
+
return result.endsWith("/") && !relative.endsWith("/") ? `${relative}/` : relative;
|
|
7006
|
+
};
|
|
7007
|
+
const insideGitRepo = (fs, searchPath) => Effect.gen(function* () {
|
|
7008
|
+
for (let current = searchPath;; current = posix.dirname(current)) {
|
|
7009
|
+
if (yield* fs.exists(posix.join(current, ".git")).pipe(Effect.orElseSucceed(() => false))) return true;
|
|
7010
|
+
if (posix.dirname(current) === current) return false;
|
|
7011
|
+
}
|
|
7012
|
+
});
|
|
7013
|
+
const findHandler = (params) => Effect.gen(function* () {
|
|
7014
|
+
const fs = yield* io_exports.SandboxIO.FileSystem;
|
|
7015
|
+
const shell = yield* io_exports.SandboxIO.Shell;
|
|
7016
|
+
const sandbox = yield* io_exports.SandboxIO.Current;
|
|
7017
|
+
const searchPath = posix.resolve(sandbox.cwd, normalize$1(params.path || "."));
|
|
7018
|
+
const limit = Math.max(1, Math.trunc(params.limit ?? DEFAULT_LIMIT$2));
|
|
7019
|
+
if (!(yield* fs.exists(searchPath).pipe(Effect.orElseSucceed(() => false)))) return yield* fail$3(`Path not found: ${searchPath}`);
|
|
7020
|
+
const argv = [
|
|
7021
|
+
"--glob",
|
|
7022
|
+
"--color=never",
|
|
7023
|
+
"--hidden",
|
|
7024
|
+
...(yield* insideGitRepo(fs, searchPath)) ? [] : ["--no-require-git"]
|
|
7025
|
+
];
|
|
7026
|
+
argv.push("--max-results", String(limit));
|
|
7027
|
+
let pattern = params.pattern;
|
|
7028
|
+
if (pattern.includes("/")) {
|
|
7029
|
+
argv.push("--full-path");
|
|
7030
|
+
if (!pattern.startsWith("/") && !pattern.startsWith("**/") && pattern !== "**") pattern = `**/${pattern}`;
|
|
7031
|
+
}
|
|
7032
|
+
argv.push("--", pattern, searchPath);
|
|
7033
|
+
const run = yield* withCommand("fd", sandbox, shell, fs, (command) => shell.exec((0, shell_exports.quoteArgv)([command, ...argv])));
|
|
7034
|
+
if ("_tag" in run) return yield* fail$3(run.message);
|
|
7035
|
+
const output = run.stdout.split("\n").map((line) => line.replace(/\r$/, "").trim()).filter((line) => line.length > 0);
|
|
7036
|
+
const lines = output.filter((line) => !line.startsWith(FD_ERROR));
|
|
7037
|
+
const errors = [run.stderr.trim(), ...output.filter((line) => line.startsWith(FD_ERROR))].filter((text) => text.length > 0).join("\n");
|
|
7038
|
+
if (run.exitCode !== 0 && lines.length === 0) return yield* fail$3(errors || `fd exited with code ${run.exitCode}`);
|
|
7039
|
+
if (lines.length === 0) return { text: "No files found matching pattern" };
|
|
7040
|
+
const results = lines.map((line) => relativize(line, searchPath)).sort();
|
|
7041
|
+
const resultLimitReached = results.length >= limit;
|
|
7042
|
+
const truncation = truncateHead(results.join("\n"), { maxLines: Number.MAX_SAFE_INTEGER });
|
|
7043
|
+
const notices = [];
|
|
7044
|
+
const details = {};
|
|
7045
|
+
if (resultLimitReached) {
|
|
7046
|
+
notices.push(`${limit} results limit reached. Use limit=${limit * 2} for more, or refine pattern`);
|
|
7047
|
+
details.resultLimitReached = limit;
|
|
7048
|
+
}
|
|
7049
|
+
if (truncation.truncated) {
|
|
7050
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
7051
|
+
details.truncation = truncation;
|
|
7052
|
+
}
|
|
7053
|
+
const text = notices.length > 0 ? `${truncation.content}\n\n[${notices.join(". ")}]` : truncation.content;
|
|
7054
|
+
return Object.keys(details).length > 0 ? {
|
|
7055
|
+
text,
|
|
7056
|
+
details
|
|
7057
|
+
} : { text };
|
|
7058
|
+
}).pipe(Effect.catchTag("ShellError", (error) => Effect.die(error)));
|
|
7059
|
+
/** The find tool: definition + handler, wired the testable (def/exec split) way. */
|
|
7060
|
+
const findTool = tool_exports.implement(findDef, findHandler);
|
|
7061
|
+
const findPlugin = (0, plugin_exports$1.define)({
|
|
7062
|
+
id: "codework.tool.find",
|
|
7063
|
+
kind: "tool",
|
|
7064
|
+
optIn: true,
|
|
7065
|
+
setup: Effect.fn("FindPlugin.setup")(function* (ctx) {
|
|
7066
|
+
const mounted = Layer.mergeAll(Layer.succeed(io_exports.SandboxIO.FileSystem, yield* io_exports.SandboxIO.FileSystem), Layer.succeed(io_exports.SandboxIO.Shell, yield* io_exports.SandboxIO.Shell), Layer.succeed(io_exports.SandboxIO.Current, yield* io_exports.SandboxIO.Current), Layer.succeed(HostBin, ctx.paths.bin));
|
|
7067
|
+
ctx.plugin.tools.add(tool_exports.provide(findTool, mounted));
|
|
7068
|
+
})
|
|
7069
|
+
});
|
|
7070
|
+
//#endregion
|
|
7071
|
+
//#region src/plugin/builtin/tool/grep.ts
|
|
7072
|
+
/**
|
|
7073
|
+
* The built-in grep plugin: ripgrep in the mounted sandbox — the host's or the
|
|
7074
|
+
* remote's own binary, or just-bash's built-in `rg` on a virtual mount. Only the
|
|
7075
|
+
* first `limit` match events leave the sandbox; context lines are read through
|
|
7076
|
+
* `scanLines`, so only their window crosses.
|
|
7077
|
+
*/
|
|
7078
|
+
const GrepParams = Schema.Struct({
|
|
7079
|
+
pattern: Schema.String.annotate({ description: "Search pattern (regex or literal string)" }),
|
|
7080
|
+
path: Schema.optional(Schema.String.annotate({ description: "Directory or file to search (default: current directory)" })),
|
|
7081
|
+
glob: Schema.optional(Schema.String.annotate({ description: "Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'" })),
|
|
7082
|
+
ignoreCase: Schema.optional(Schema.Boolean.annotate({ description: "Case-insensitive search (default: false)" })),
|
|
7083
|
+
literal: Schema.optional(Schema.Boolean.annotate({ description: "Treat pattern as literal string instead of regex (default: false)" })),
|
|
7084
|
+
context: Schema.optional(Schema.Finite.annotate({ description: "Number of lines to show before and after each match (default: 0)" })),
|
|
7085
|
+
limit: Schema.optional(Schema.Finite.annotate({ description: "Maximum number of matches to return (default: 100)" }))
|
|
7086
|
+
});
|
|
7087
|
+
const GrepDetails = Schema.Struct({
|
|
7088
|
+
truncation: Schema.optional(Schema.Unknown),
|
|
7089
|
+
matchLimitReached: Schema.optional(Schema.Finite),
|
|
7090
|
+
linesTruncated: Schema.optional(Schema.Boolean)
|
|
7091
|
+
});
|
|
7092
|
+
const GrepSuccess = Schema.Struct({
|
|
7093
|
+
text: Schema.String,
|
|
7094
|
+
details: Schema.optional(GrepDetails)
|
|
7095
|
+
});
|
|
7096
|
+
var GrepFailed = class extends Schema.TaggedError()("GrepFailed", { message: Schema.String }) {};
|
|
7097
|
+
const DEFAULT_LIMIT$1 = 100;
|
|
7098
|
+
const grepDef = tool_exports.define({
|
|
7099
|
+
name: "grep",
|
|
7100
|
+
label: "grep",
|
|
7101
|
+
description: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT$1} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to 500 chars.`,
|
|
7102
|
+
promptSnippet: "Search file contents for patterns (respects .gitignore)",
|
|
7103
|
+
parameters: GrepParams,
|
|
7104
|
+
constrainedSampling: {
|
|
7105
|
+
type: "json_schema",
|
|
7106
|
+
strict: "prefer"
|
|
7107
|
+
},
|
|
7108
|
+
success: GrepSuccess,
|
|
7109
|
+
failure: GrepFailed,
|
|
7110
|
+
encodeContent: (success) => [{
|
|
7111
|
+
type: "text",
|
|
7112
|
+
text: success.text
|
|
5458
7113
|
}],
|
|
5459
7114
|
encodeFailureContent: (failure) => [{
|
|
5460
7115
|
type: "text",
|
|
5461
|
-
text: failure.
|
|
5462
|
-
}]
|
|
7116
|
+
text: failure.message
|
|
7117
|
+
}],
|
|
7118
|
+
encodeDetails: (success) => success.details
|
|
5463
7119
|
});
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
7120
|
+
const fail$2 = (message) => Effect.fail(new GrepFailed({ message }));
|
|
7121
|
+
/** One `--json` match event; anything else (begin/end/summary, a non-UTF-8 path) is skipped. */
|
|
7122
|
+
const parseMatch = (line) => {
|
|
7123
|
+
try {
|
|
7124
|
+
const event = JSON.parse(line);
|
|
7125
|
+
const path = event.data?.path?.text;
|
|
7126
|
+
const number = event.data?.line_number;
|
|
7127
|
+
const text = event.data?.lines?.text;
|
|
7128
|
+
if (event.type !== "match" || typeof path !== "string" || typeof number !== "number") return void 0;
|
|
7129
|
+
return {
|
|
7130
|
+
path,
|
|
7131
|
+
line: number,
|
|
7132
|
+
text: typeof text === "string" ? text : ""
|
|
7133
|
+
};
|
|
7134
|
+
} catch {
|
|
7135
|
+
return;
|
|
7136
|
+
}
|
|
5469
7137
|
};
|
|
5470
|
-
/**
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
7138
|
+
/**
|
|
7139
|
+
* The search runs as one pipeline so a broad pattern never ships its whole result:
|
|
7140
|
+
* only match events pass, and `head` stops ripgrep at the limit. Some backends
|
|
7141
|
+
* merge stderr into stdout, so nothing relies on the two staying apart: ripgrep's
|
|
7142
|
+
* status rides the pipe as a last event, and its stderr waits in `errors` until the
|
|
7143
|
+
* pipeline is done, where it can no longer land inside a match.
|
|
7144
|
+
*/
|
|
7145
|
+
const pipeline = (argv, limit, errors) => [
|
|
7146
|
+
`mkdir -p ${(0, shell_exports.quote)(posix.dirname(errors))} 2>/dev/null`,
|
|
7147
|
+
`{ : > ${(0, shell_exports.quote)(errors)}; } 2>/dev/null || { echo ${(0, shell_exports.quote)(`cannot write ${errors}`)} >&2; exit 2; }`,
|
|
7148
|
+
`{ ${(0, shell_exports.quoteArgv)(argv)} 2>${(0, shell_exports.quote)(errors)}; echo '{"type":"rc","code":'"$?"'}'; } | grep -E '^\\{"type":"(match|rc)"' | head -n ${limit}`,
|
|
7149
|
+
`cat ${(0, shell_exports.quote)(errors)} >&2 2>/dev/null`,
|
|
7150
|
+
`rm -f ${(0, shell_exports.quote)(errors)}`
|
|
7151
|
+
].join("; ");
|
|
7152
|
+
const STATUS = /^\{"type":"rc","code":(\d+)\}$/;
|
|
7153
|
+
/** The pipeline's output: match events, ripgrep's status, and anything else, which is its stderr. */
|
|
7154
|
+
const parse$2 = (stdout, stderr) => {
|
|
7155
|
+
const matches = [];
|
|
7156
|
+
const errors = [];
|
|
7157
|
+
let status;
|
|
7158
|
+
for (const line of stdout.split("\n")) {
|
|
7159
|
+
const rc = STATUS.exec(line);
|
|
7160
|
+
if (rc !== null) status ??= Number(rc[1]);
|
|
7161
|
+
else if (line.startsWith("{\"type\":\"match\"")) {
|
|
7162
|
+
const match = parseMatch(line);
|
|
7163
|
+
if (match !== void 0) matches.push(match);
|
|
7164
|
+
} else if (line.trim().length > 0) errors.push(line);
|
|
5477
7165
|
}
|
|
5478
|
-
|
|
5479
|
-
|
|
7166
|
+
return {
|
|
7167
|
+
matches,
|
|
7168
|
+
status,
|
|
7169
|
+
errors: [stderr.trim(), ...errors].filter((text) => text.length > 0).join("\n")
|
|
7170
|
+
};
|
|
5480
7171
|
};
|
|
5481
|
-
/**
|
|
5482
|
-
const
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
const
|
|
5490
|
-
const
|
|
5491
|
-
|
|
5492
|
-
|
|
7172
|
+
/** A line as shown: no line ending, cut to {@link GREP_MAX_LINE_LENGTH}. */
|
|
7173
|
+
const shown = (line) => truncateLine(line.replace(/\r\n/g, "\n").replace(/\r/g, "").replace(/\n$/, ""));
|
|
7174
|
+
/**
|
|
7175
|
+
* The match with its context lines. The window is read in one bounded scan; a
|
|
7176
|
+
* line it could not fit is read on its own, and a line too long even for that is
|
|
7177
|
+
* marked rather than shown blank. The match line itself is ripgrep's.
|
|
7178
|
+
*/
|
|
7179
|
+
const contextBlock = (fs, match, context, label) => Effect.gen(function* () {
|
|
7180
|
+
const startLine = Math.max(0, match.line - 1 - context);
|
|
7181
|
+
const scan = yield* fs.scanLines(match.path, {
|
|
7182
|
+
startLine,
|
|
7183
|
+
endLine: match.line + context,
|
|
7184
|
+
maxBytes: DEFAULT_MAX_BYTES
|
|
7185
|
+
}).pipe(Effect.option);
|
|
7186
|
+
if (scan._tag === "None" || scan.value.totalLines === 0) return {
|
|
7187
|
+
lines: [`${label}:${match.line}: (unable to read file)`],
|
|
5493
7188
|
truncated: false
|
|
5494
7189
|
};
|
|
5495
|
-
const
|
|
7190
|
+
const window = scan.value.lines === 0 ? [] : scan.value.text.split("\n");
|
|
7191
|
+
const lineAt = (index) => index - startLine < window.length ? Effect.succeed(window[index - startLine]) : fs.scanLines(match.path, {
|
|
7192
|
+
startLine: index,
|
|
7193
|
+
endLine: index + 1,
|
|
7194
|
+
maxBytes: DEFAULT_MAX_BYTES
|
|
7195
|
+
}).pipe(Effect.map((one) => one.lines === 1 ? one.text : void 0), Effect.orElseSucceed(() => void 0));
|
|
7196
|
+
const end = Math.min(scan.value.totalLines, match.line + context);
|
|
7197
|
+
const block = [];
|
|
7198
|
+
let truncated = false;
|
|
7199
|
+
for (let current = startLine + 1; current <= end; current++) {
|
|
7200
|
+
const line = current === match.line ? match.text : yield* lineAt(current - 1);
|
|
7201
|
+
const { text, wasTruncated } = line === void 0 ? {
|
|
7202
|
+
text: "... [truncated]",
|
|
7203
|
+
wasTruncated: true
|
|
7204
|
+
} : shown(line);
|
|
7205
|
+
if (wasTruncated) truncated = true;
|
|
7206
|
+
block.push(current === match.line ? `${label}:${current}: ${text}` : `${label}-${current}- ${text}`);
|
|
7207
|
+
}
|
|
5496
7208
|
return {
|
|
5497
|
-
|
|
5498
|
-
truncated
|
|
5499
|
-
...fullOutputPath !== void 0 ? { fullOutputPath } : {}
|
|
7209
|
+
lines: block,
|
|
7210
|
+
truncated
|
|
5500
7211
|
};
|
|
5501
7212
|
});
|
|
5502
|
-
|
|
5503
|
-
const
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
7213
|
+
const grepHandler = (params) => Effect.gen(function* () {
|
|
7214
|
+
const fs = yield* io_exports.SandboxIO.FileSystem;
|
|
7215
|
+
const shell = yield* io_exports.SandboxIO.Shell;
|
|
7216
|
+
const sandbox = yield* io_exports.SandboxIO.Current;
|
|
7217
|
+
const searchPath = posix.resolve(sandbox.cwd, normalize$1(params.path || "."));
|
|
7218
|
+
const stat = yield* fs.stat(searchPath).pipe(Effect.option);
|
|
7219
|
+
if (stat._tag === "None") return yield* fail$2(`Path not found: ${searchPath}`);
|
|
7220
|
+
const isDirectory = stat.value.isDirectory;
|
|
7221
|
+
const context = params.context && params.context > 0 ? Math.trunc(params.context) : 0;
|
|
7222
|
+
const limit = Math.max(1, Math.trunc(params.limit ?? DEFAULT_LIMIT$1));
|
|
7223
|
+
const errors = yield* path$1(sandbox.spillPath, "codework-grep");
|
|
7224
|
+
const run = yield* withCommand("rg", sandbox, shell, fs, (command) => Effect.gen(function* () {
|
|
7225
|
+
const argv = [
|
|
7226
|
+
command,
|
|
7227
|
+
"--json",
|
|
7228
|
+
"--line-number",
|
|
7229
|
+
"--hidden",
|
|
7230
|
+
"--sort",
|
|
7231
|
+
"path"
|
|
7232
|
+
];
|
|
7233
|
+
argv.push(params.ignoreCase ? "--ignore-case" : "-s");
|
|
7234
|
+
if (params.literal) argv.push("--fixed-strings");
|
|
7235
|
+
if (params.glob) argv.push("--glob", params.glob);
|
|
7236
|
+
argv.push("-e", params.pattern, searchPath);
|
|
7237
|
+
const result = yield* shell.exec(pipeline(argv, limit, errors));
|
|
7238
|
+
const parsed = parse$2(result.stdout, result.stderr);
|
|
7239
|
+
return {
|
|
7240
|
+
...parsed,
|
|
7241
|
+
exitCode: parsed.status ?? result.exitCode
|
|
7242
|
+
};
|
|
7243
|
+
}));
|
|
7244
|
+
if ("_tag" in run) return yield* fail$2(run.message);
|
|
7245
|
+
const { matches } = run;
|
|
7246
|
+
const matchLimitReached = matches.length >= limit;
|
|
7247
|
+
if (!matchLimitReached && run.exitCode !== 0 && run.exitCode !== 1) return yield* fail$2(run.errors || `ripgrep exited with code ${run.exitCode}`);
|
|
7248
|
+
if (matches.length === 0) return { text: "No matches found" };
|
|
7249
|
+
const label = (path) => {
|
|
7250
|
+
if (isDirectory) {
|
|
7251
|
+
const relative = posix.relative(searchPath, path);
|
|
7252
|
+
if (relative && !relative.startsWith("..")) return relative;
|
|
7253
|
+
}
|
|
7254
|
+
return posix.basename(path);
|
|
7255
|
+
};
|
|
7256
|
+
const output = [];
|
|
7257
|
+
let linesTruncated = false;
|
|
7258
|
+
for (const match of matches) if (context === 0) {
|
|
7259
|
+
const { text, wasTruncated } = shown(match.text);
|
|
7260
|
+
if (wasTruncated) linesTruncated = true;
|
|
7261
|
+
output.push(`${label(match.path)}:${match.line}: ${text}`);
|
|
7262
|
+
} else {
|
|
7263
|
+
const block = yield* contextBlock(fs, match, context, label(match.path));
|
|
7264
|
+
if (block.truncated) linesTruncated = true;
|
|
7265
|
+
output.push(...block.lines);
|
|
7266
|
+
}
|
|
7267
|
+
const truncation = truncateHead(output.join("\n"), { maxLines: Number.MAX_SAFE_INTEGER });
|
|
7268
|
+
const notices = [];
|
|
7269
|
+
const details = {};
|
|
7270
|
+
if (matchLimitReached) {
|
|
7271
|
+
notices.push(`${limit} matches limit reached. Use limit=${limit * 2} for more, or refine pattern`);
|
|
7272
|
+
details.matchLimitReached = limit;
|
|
7273
|
+
}
|
|
7274
|
+
if (truncation.truncated) {
|
|
7275
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
7276
|
+
details.truncation = truncation;
|
|
7277
|
+
}
|
|
7278
|
+
if (linesTruncated) {
|
|
7279
|
+
notices.push(`Some lines truncated to 500 chars. Use read tool to see full lines`);
|
|
7280
|
+
details.linesTruncated = true;
|
|
7281
|
+
}
|
|
7282
|
+
const text = notices.length > 0 ? `${truncation.content}\n\n[${notices.join(". ")}]` : truncation.content;
|
|
7283
|
+
return Object.keys(details).length > 0 ? {
|
|
7284
|
+
text,
|
|
7285
|
+
details
|
|
7286
|
+
} : { text };
|
|
7287
|
+
}).pipe(Effect.catchTag("ShellError", (error) => Effect.die(error)));
|
|
7288
|
+
/** The grep tool: definition + handler, wired the testable (def/exec split) way. */
|
|
7289
|
+
const grepTool = tool_exports.implement(grepDef, grepHandler);
|
|
7290
|
+
const grepPlugin = (0, plugin_exports$1.define)({
|
|
7291
|
+
id: "codework.tool.grep",
|
|
7292
|
+
kind: "tool",
|
|
7293
|
+
optIn: true,
|
|
7294
|
+
setup: Effect.fn("GrepPlugin.setup")(function* (ctx) {
|
|
7295
|
+
const mounted = Layer.mergeAll(Layer.succeed(io_exports.SandboxIO.FileSystem, yield* io_exports.SandboxIO.FileSystem), Layer.succeed(io_exports.SandboxIO.Shell, yield* io_exports.SandboxIO.Shell), Layer.succeed(io_exports.SandboxIO.Current, yield* io_exports.SandboxIO.Current), Layer.succeed(HostBin, ctx.paths.bin));
|
|
7296
|
+
ctx.plugin.tools.add(tool_exports.provide(grepTool, mounted));
|
|
7297
|
+
})
|
|
7298
|
+
});
|
|
7299
|
+
//#endregion
|
|
7300
|
+
//#region src/plugin/builtin/tool/ls.ts
|
|
7301
|
+
/** The built-in ls plugin: one directory through the sandbox filesystem, no binaries involved. */
|
|
7302
|
+
const LsParams = Schema.Struct({
|
|
7303
|
+
path: Schema.optional(Schema.String.annotate({ description: "Directory to list (default: current directory)" })),
|
|
7304
|
+
limit: Schema.optional(Schema.Finite.annotate({ description: "Maximum number of entries to return (default: 500)" }))
|
|
7305
|
+
});
|
|
7306
|
+
const LsDetails = Schema.Struct({
|
|
7307
|
+
truncation: Schema.optional(Schema.Unknown),
|
|
7308
|
+
entryLimitReached: Schema.optional(Schema.Finite)
|
|
7309
|
+
});
|
|
7310
|
+
const LsSuccess = Schema.Struct({
|
|
7311
|
+
text: Schema.String,
|
|
7312
|
+
details: Schema.optional(LsDetails)
|
|
7313
|
+
});
|
|
7314
|
+
var LsFailed = class extends Schema.TaggedError()("LsFailed", { message: Schema.String }) {};
|
|
7315
|
+
const DEFAULT_LIMIT = 500;
|
|
7316
|
+
const lsDef = tool_exports.define({
|
|
7317
|
+
name: "ls",
|
|
7318
|
+
label: "ls",
|
|
7319
|
+
description: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,
|
|
7320
|
+
promptSnippet: "List directory contents",
|
|
7321
|
+
parameters: LsParams,
|
|
7322
|
+
constrainedSampling: {
|
|
7323
|
+
type: "json_schema",
|
|
7324
|
+
strict: "prefer"
|
|
7325
|
+
},
|
|
7326
|
+
success: LsSuccess,
|
|
7327
|
+
failure: LsFailed,
|
|
7328
|
+
encodeContent: (success) => [{
|
|
7329
|
+
type: "text",
|
|
7330
|
+
text: success.text
|
|
7331
|
+
}],
|
|
7332
|
+
encodeFailureContent: (failure) => [{
|
|
7333
|
+
type: "text",
|
|
7334
|
+
text: failure.message
|
|
7335
|
+
}],
|
|
7336
|
+
encodeDetails: (success) => success.details
|
|
7337
|
+
});
|
|
7338
|
+
const fail$1 = (message) => Effect.fail(new LsFailed({ message }));
|
|
7339
|
+
const lsHandler = (params) => Effect.gen(function* () {
|
|
7340
|
+
const fs = yield* io_exports.SandboxIO.FileSystem;
|
|
7341
|
+
const sandbox = yield* io_exports.SandboxIO.Current;
|
|
7342
|
+
const dirPath = posix.resolve(sandbox.cwd, normalize$1(params.path || "."));
|
|
7343
|
+
const limit = params.limit ?? DEFAULT_LIMIT;
|
|
7344
|
+
const stat = yield* fs.stat(dirPath).pipe(Effect.option);
|
|
7345
|
+
if (stat._tag === "None") return yield* fail$1(`Path not found: ${dirPath}`);
|
|
7346
|
+
if (!stat.value.isDirectory) return yield* fail$1(`Not a directory: ${dirPath}`);
|
|
7347
|
+
const read = yield* fs.readdir(dirPath).pipe(Effect.result);
|
|
7348
|
+
if (read._tag === "Failure") {
|
|
7349
|
+
const cause = read.failure.cause;
|
|
7350
|
+
return yield* fail$1(`Cannot read directory: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
7351
|
+
}
|
|
7352
|
+
const entries = [...read.success].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
7353
|
+
const results = [];
|
|
7354
|
+
let entryLimitReached = false;
|
|
7355
|
+
for (const entry of entries) {
|
|
7356
|
+
if (results.length >= limit) {
|
|
7357
|
+
entryLimitReached = true;
|
|
7358
|
+
break;
|
|
7359
|
+
}
|
|
7360
|
+
const entryStat = yield* fs.stat(posix.join(dirPath, entry)).pipe(Effect.option);
|
|
7361
|
+
if (entryStat._tag === "None") continue;
|
|
7362
|
+
results.push(entryStat.value.isDirectory ? `${entry}/` : entry);
|
|
7363
|
+
}
|
|
7364
|
+
if (results.length === 0) return { text: "(empty directory)" };
|
|
7365
|
+
const truncation = truncateHead(results.join("\n"), { maxLines: Number.MAX_SAFE_INTEGER });
|
|
7366
|
+
const notices = [];
|
|
7367
|
+
const details = {};
|
|
7368
|
+
if (entryLimitReached) {
|
|
7369
|
+
notices.push(`${limit} entries limit reached. Use limit=${limit * 2} for more`);
|
|
7370
|
+
details.entryLimitReached = limit;
|
|
7371
|
+
}
|
|
7372
|
+
if (truncation.truncated) {
|
|
7373
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
7374
|
+
details.truncation = truncation;
|
|
7375
|
+
}
|
|
7376
|
+
const text = notices.length > 0 ? `${truncation.content}\n\n[${notices.join(". ")}]` : truncation.content;
|
|
7377
|
+
return Object.keys(details).length > 0 ? {
|
|
7378
|
+
text,
|
|
7379
|
+
details
|
|
7380
|
+
} : { text };
|
|
7381
|
+
});
|
|
7382
|
+
/** The ls tool: definition + handler, wired the testable (def/exec split) way. */
|
|
7383
|
+
const lsTool = tool_exports.implement(lsDef, lsHandler);
|
|
7384
|
+
const lsPlugin = (0, plugin_exports$1.define)({
|
|
7385
|
+
id: "codework.tool.ls",
|
|
7386
|
+
kind: "tool",
|
|
7387
|
+
optIn: true,
|
|
7388
|
+
setup: Effect.fn("LsPlugin.setup")(function* (ctx) {
|
|
7389
|
+
const mounted = Layer.merge(Layer.succeed(io_exports.SandboxIO.FileSystem, yield* io_exports.SandboxIO.FileSystem), Layer.succeed(io_exports.SandboxIO.Current, yield* io_exports.SandboxIO.Current));
|
|
7390
|
+
ctx.plugin.tools.add(tool_exports.provide(lsTool, mounted));
|
|
7391
|
+
})
|
|
7392
|
+
});
|
|
7393
|
+
//#endregion
|
|
7394
|
+
//#region src/tool/image/mime.ts
|
|
7395
|
+
/**
|
|
7396
|
+
* Image formats a model takes inline, told apart by their bytes, never by the
|
|
7397
|
+
* file name: a text file that starts with `GIF` is still text.
|
|
7398
|
+
*/
|
|
7399
|
+
const PNG_SIGNATURE = [
|
|
7400
|
+
137,
|
|
7401
|
+
80,
|
|
7402
|
+
78,
|
|
7403
|
+
71,
|
|
7404
|
+
13,
|
|
7405
|
+
10,
|
|
7406
|
+
26,
|
|
7407
|
+
10
|
|
7408
|
+
];
|
|
7409
|
+
/** Bytes every check except the APNG chunk walk needs: BMP reads up to offset 29. */
|
|
7410
|
+
const HEADER_BYTES = 32;
|
|
7411
|
+
const BLOCK_BYTES = 65536;
|
|
7412
|
+
/**
|
|
7413
|
+
* The image MIME type of a whole file, reading only its header and, for PNG, the
|
|
7414
|
+
* chunk headers up to the first `acTL` or `IDAT`. Animated PNGs are not images
|
|
7415
|
+
* here: a model would see only their first frame.
|
|
7416
|
+
*/
|
|
7417
|
+
const detectOf = (source) => Effect.gen(function* () {
|
|
7418
|
+
const header = yield* source.read(0, HEADER_BYTES);
|
|
7419
|
+
if (!startsWith(header, PNG_SIGNATURE)) return detect(header);
|
|
7420
|
+
return isPng(header) && !(yield* isAnimatedPngOf(source)) ? "image/png" : void 0;
|
|
7421
|
+
});
|
|
7422
|
+
const isAnimatedPngOf = (source) => Effect.gen(function* () {
|
|
7423
|
+
let block = /* @__PURE__ */ new Uint8Array(0);
|
|
7424
|
+
let blockStart = 0;
|
|
7425
|
+
let offset = PNG_SIGNATURE.length;
|
|
7426
|
+
while (offset + 8 <= source.size) {
|
|
7427
|
+
if (offset < blockStart || offset + 8 > blockStart + block.length) {
|
|
7428
|
+
blockStart = offset;
|
|
7429
|
+
block = yield* source.read(offset, BLOCK_BYTES);
|
|
7430
|
+
}
|
|
7431
|
+
const chunkHeader = block.subarray(offset - blockStart, offset - blockStart + 8);
|
|
7432
|
+
if (chunkHeader.length < 8) return false;
|
|
7433
|
+
if (startsWithAscii(chunkHeader, 4, "acTL")) return true;
|
|
7434
|
+
if (startsWithAscii(chunkHeader, 4, "IDAT")) return false;
|
|
7435
|
+
const nextOffset = offset + 8 + readUint32BE(chunkHeader, 0) + 4;
|
|
7436
|
+
if (nextOffset <= offset || nextOffset > source.size) return false;
|
|
7437
|
+
offset = nextOffset;
|
|
7438
|
+
}
|
|
7439
|
+
return false;
|
|
7440
|
+
});
|
|
7441
|
+
/** The image MIME type of a header; a PNG still needs {@link isAnimatedPngOf}. */
|
|
7442
|
+
const detect = (buffer) => {
|
|
7443
|
+
if (startsWith(buffer, [
|
|
7444
|
+
255,
|
|
7445
|
+
216,
|
|
7446
|
+
255
|
|
7447
|
+
])) return buffer[3] === 247 ? void 0 : "image/jpeg";
|
|
7448
|
+
if (startsWith(buffer, PNG_SIGNATURE)) return isPng(buffer) ? "image/png" : void 0;
|
|
7449
|
+
if (startsWithAscii(buffer, 0, "GIF87a") || startsWithAscii(buffer, 0, "GIF89a")) return "image/gif";
|
|
7450
|
+
if (startsWithAscii(buffer, 0, "RIFF") && startsWithAscii(buffer, 8, "WEBP")) return "image/webp";
|
|
7451
|
+
if (startsWithAscii(buffer, 0, "BM") && isBmp(buffer)) return "image/bmp";
|
|
7452
|
+
};
|
|
7453
|
+
const isPng = (buffer) => buffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, "IHDR");
|
|
7454
|
+
const isBmp = (buffer) => {
|
|
7455
|
+
if (buffer.length < 26) return false;
|
|
7456
|
+
const declaredFileSize = readUint32LE(buffer, 2);
|
|
7457
|
+
const pixelDataOffset = readUint32LE(buffer, 10);
|
|
7458
|
+
const dibHeaderSize = readUint32LE(buffer, 14);
|
|
7459
|
+
if (declaredFileSize !== 0 && declaredFileSize < 26) return false;
|
|
7460
|
+
if (pixelDataOffset < 14 + dibHeaderSize) return false;
|
|
7461
|
+
if (declaredFileSize !== 0 && pixelDataOffset >= declaredFileSize) return false;
|
|
7462
|
+
let colorPlanes;
|
|
7463
|
+
let bitsPerPixel;
|
|
7464
|
+
if (dibHeaderSize === 12) {
|
|
7465
|
+
colorPlanes = readUint16LE(buffer, 22);
|
|
7466
|
+
bitsPerPixel = readUint16LE(buffer, 24);
|
|
7467
|
+
} else if (dibHeaderSize >= 40 && dibHeaderSize <= 124) {
|
|
7468
|
+
if (buffer.length < 30) return false;
|
|
7469
|
+
colorPlanes = readUint16LE(buffer, 26);
|
|
7470
|
+
bitsPerPixel = readUint16LE(buffer, 28);
|
|
7471
|
+
} else return false;
|
|
7472
|
+
return colorPlanes === 1 && [
|
|
7473
|
+
1,
|
|
7474
|
+
4,
|
|
7475
|
+
8,
|
|
7476
|
+
16,
|
|
7477
|
+
24,
|
|
7478
|
+
32
|
|
7479
|
+
].includes(bitsPerPixel);
|
|
7480
|
+
};
|
|
7481
|
+
const readUint16LE = (buffer, offset) => (buffer[offset] ?? 0) + ((buffer[offset + 1] ?? 0) << 8);
|
|
7482
|
+
const readUint32BE = (buffer, offset) => (buffer[offset] ?? 0) * 16777216 + ((buffer[offset + 1] ?? 0) << 16) + ((buffer[offset + 2] ?? 0) << 8) + (buffer[offset + 3] ?? 0);
|
|
7483
|
+
const readUint32LE = (buffer, offset) => (buffer[offset] ?? 0) + ((buffer[offset + 1] ?? 0) << 8) + ((buffer[offset + 2] ?? 0) << 16) + (buffer[offset + 3] ?? 0) * 16777216;
|
|
7484
|
+
const startsWith = (buffer, bytes) => buffer.length >= bytes.length && bytes.every((byte, index) => buffer[index] === byte);
|
|
7485
|
+
const startsWithAscii = (buffer, offset, text) => {
|
|
7486
|
+
if (buffer.length < offset + text.length) return false;
|
|
7487
|
+
for (let index = 0; index < text.length; index++) if (buffer[offset + index] !== text.charCodeAt(index)) return false;
|
|
7488
|
+
return true;
|
|
7489
|
+
};
|
|
7490
|
+
//#endregion
|
|
7491
|
+
//#region src/tool/image/index.ts
|
|
7492
|
+
const INLINE = /* @__PURE__ */ new Set([
|
|
7493
|
+
"image/png",
|
|
7494
|
+
"image/jpeg",
|
|
7495
|
+
"image/gif",
|
|
7496
|
+
"image/webp"
|
|
7497
|
+
]);
|
|
7498
|
+
const toPng = (bytes) => load$4.pipe(Effect.map((photon) => {
|
|
7499
|
+
if (photon === void 0) return void 0;
|
|
7500
|
+
try {
|
|
7501
|
+
const image = decode(photon, bytes);
|
|
7502
|
+
try {
|
|
7503
|
+
return new Uint8Array(image.get_bytes());
|
|
7504
|
+
} finally {
|
|
7505
|
+
image.free();
|
|
7506
|
+
}
|
|
7507
|
+
} catch {
|
|
7508
|
+
return;
|
|
7509
|
+
}
|
|
7510
|
+
}));
|
|
7511
|
+
/**
|
|
7512
|
+
* Turn an image file into what a model takes inline: a supported format, within
|
|
7513
|
+
* the provider's dimension and size limits. A failure is a note for the model,
|
|
7514
|
+
* never an error — the read itself succeeded.
|
|
7515
|
+
*/
|
|
7516
|
+
const inline = Effect.fn("Image.inline")(function* (bytes, mimeType) {
|
|
7517
|
+
let source = {
|
|
7518
|
+
bytes,
|
|
7519
|
+
mimeType,
|
|
7520
|
+
convertedFrom: void 0
|
|
7521
|
+
};
|
|
7522
|
+
if (!INLINE.has(mimeType)) {
|
|
7523
|
+
const png = yield* toPng(bytes);
|
|
7524
|
+
if (png === void 0) return {
|
|
7525
|
+
ok: false,
|
|
7526
|
+
message: "[Image omitted: could not be converted to a supported inline image format.]"
|
|
7527
|
+
};
|
|
7528
|
+
source = {
|
|
7529
|
+
bytes: png,
|
|
7530
|
+
mimeType: "image/png",
|
|
7531
|
+
convertedFrom: mimeType
|
|
7532
|
+
};
|
|
7533
|
+
}
|
|
7534
|
+
const resized = yield* resize(source.bytes, source.mimeType);
|
|
7535
|
+
if (resized === null) return {
|
|
7536
|
+
ok: false,
|
|
7537
|
+
message: "[Image omitted: could not be resized below the inline image size limit.]"
|
|
5507
7538
|
};
|
|
7539
|
+
const hints = [];
|
|
7540
|
+
if (source.convertedFrom !== void 0 && source.convertedFrom !== resized.mimeType) hints.push(`[Image converted from ${source.convertedFrom} to ${resized.mimeType}.]`);
|
|
7541
|
+
const note = dimensionNote(resized);
|
|
7542
|
+
if (note !== void 0) hints.push(note);
|
|
5508
7543
|
return {
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
7544
|
+
ok: true,
|
|
7545
|
+
data: resized.data,
|
|
7546
|
+
mimeType: resized.mimeType,
|
|
7547
|
+
hints
|
|
7548
|
+
};
|
|
7549
|
+
});
|
|
7550
|
+
//#endregion
|
|
7551
|
+
//#region src/plugin/builtin/tool/read.ts
|
|
7552
|
+
/**
|
|
7553
|
+
* The built-in read plugin. Text is read through `scanLines`, so only the shown
|
|
7554
|
+
* window crosses from the sandbox; images, told apart by their bytes, go to the
|
|
7555
|
+
* model as attachments. Continuation notices are part of the text the model
|
|
7556
|
+
* reads; `details` keeps only how the text was cut.
|
|
7557
|
+
*/
|
|
7558
|
+
const ReadParams = Schema.Struct({
|
|
7559
|
+
path: Schema.String.annotate({ description: "Path to the file to read (relative or absolute)" }),
|
|
7560
|
+
offset: Schema.optional(Schema.Finite.annotate({ description: "Line number to start reading from (1-indexed)" })),
|
|
7561
|
+
limit: Schema.optional(Schema.Finite.annotate({ description: "Maximum number of lines to read" }))
|
|
7562
|
+
});
|
|
7563
|
+
/** How the shown text was cut. */
|
|
7564
|
+
const Truncation = Schema.Struct({
|
|
7565
|
+
truncated: Schema.Boolean,
|
|
7566
|
+
truncatedBy: Schema.NullOr(Schema.Literals(["lines", "bytes"])),
|
|
7567
|
+
/** Lines selected by `offset`/`limit`, before the limits. */
|
|
7568
|
+
totalLines: Schema.Finite,
|
|
7569
|
+
outputLines: Schema.Finite,
|
|
7570
|
+
outputBytes: Schema.Finite,
|
|
7571
|
+
firstLineExceedsLimit: Schema.Boolean,
|
|
7572
|
+
maxLines: Schema.Finite,
|
|
7573
|
+
maxBytes: Schema.Finite
|
|
7574
|
+
});
|
|
7575
|
+
const ReadText = Schema.Struct({
|
|
7576
|
+
type: Schema.Literal("text"),
|
|
7577
|
+
text: Schema.String,
|
|
7578
|
+
truncation: Schema.optional(Truncation)
|
|
7579
|
+
});
|
|
7580
|
+
const ReadImage = Schema.Struct({
|
|
7581
|
+
type: Schema.Literal("image"),
|
|
7582
|
+
/** What the model reads with the image: its type and any conversion/resize hints. */
|
|
7583
|
+
note: Schema.String,
|
|
7584
|
+
image: Schema.optional(Schema.Struct({
|
|
7585
|
+
data: Schema.String,
|
|
7586
|
+
mimeType: Schema.String
|
|
7587
|
+
}))
|
|
7588
|
+
});
|
|
7589
|
+
const ReadSuccess = Schema.Union([ReadText, ReadImage]);
|
|
7590
|
+
var ReadFailed = class extends Schema.TaggedError()("ReadFailed", { message: Schema.String }) {};
|
|
7591
|
+
const BINARY_PROBE_BYTES = 8e3;
|
|
7592
|
+
const readDef = tool_exports.define({
|
|
7593
|
+
name: "read",
|
|
7594
|
+
label: "read",
|
|
7595
|
+
description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp, bmp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,
|
|
7596
|
+
promptSnippet: "Read file contents",
|
|
7597
|
+
promptGuidelines: ["Use read to examine files instead of cat or sed."],
|
|
7598
|
+
parameters: ReadParams,
|
|
7599
|
+
constrainedSampling: {
|
|
7600
|
+
type: "json_schema",
|
|
7601
|
+
strict: "prefer"
|
|
7602
|
+
},
|
|
7603
|
+
success: ReadSuccess,
|
|
7604
|
+
failure: ReadFailed,
|
|
7605
|
+
encodeContent: (success) => success.type === "text" ? [{
|
|
7606
|
+
type: "text",
|
|
7607
|
+
text: success.text
|
|
7608
|
+
}] : [{
|
|
7609
|
+
type: "text",
|
|
7610
|
+
text: success.note
|
|
7611
|
+
}, ...success.image === void 0 ? [] : [{
|
|
7612
|
+
type: "image",
|
|
7613
|
+
...success.image
|
|
7614
|
+
}]],
|
|
7615
|
+
encodeFailureContent: (failure) => [{
|
|
7616
|
+
type: "text",
|
|
7617
|
+
text: failure.message
|
|
7618
|
+
}],
|
|
7619
|
+
encodeDetails: (success) => success.type === "text" && success.truncation !== void 0 ? { truncation: success.truncation } : void 0
|
|
7620
|
+
});
|
|
7621
|
+
const fail = (message) => Effect.fail(new ReadFailed({ message }));
|
|
7622
|
+
/**
|
|
7623
|
+
* Filesystem failures are the model's to see, as Pi shows them: a missing path in
|
|
7624
|
+
* one wording on every backend, anything else (EACCES, …) as the backend reported it.
|
|
7625
|
+
*/
|
|
7626
|
+
const fsFailure$1 = (path) => (error) => fail(filesystem_exports.SandboxFileSystem.isNotFoundError(error.cause) ? `ENOENT: no such file or directory, open '${path}'` : error.cause instanceof Error ? error.cause.message : `Could not read file: ${path}`);
|
|
7627
|
+
const readImage = (fs, resolved, mimeType) => Effect.gen(function* () {
|
|
7628
|
+
const bytes = yield* fs.readFileBuffer(resolved);
|
|
7629
|
+
const inline$1 = yield* inline(bytes, mimeType);
|
|
7630
|
+
if (!inline$1.ok) return {
|
|
7631
|
+
type: "image",
|
|
7632
|
+
note: `Read image file [${mimeType}]\n${inline$1.message}`
|
|
5512
7633
|
};
|
|
5513
|
-
};
|
|
5514
|
-
/** Variant A — buffered: one `exec`, truncate after completion. No live progress. */
|
|
5515
|
-
const runBuffered = (shell, params) => Effect.gen(function* () {
|
|
5516
|
-
const result = yield* shell.exec(params.command, params.timeout !== void 0 ? { timeout: Duration.seconds(params.timeout) } : void 0).pipe(Effect.catchTags({
|
|
5517
|
-
ToolShellTimeout: (timeout) => Effect.fail(new BashTimedOut({
|
|
5518
|
-
timeoutSeconds: timeout.timeoutMillis / 1e3,
|
|
5519
|
-
output: "",
|
|
5520
|
-
truncated: false
|
|
5521
|
-
})),
|
|
5522
|
-
ToolShellError: (cause) => Effect.die(cause)
|
|
5523
|
-
}));
|
|
5524
|
-
const present = yield* presentBuffered(combineOutput(result.stdout, result.stderr));
|
|
5525
|
-
if (result.exitCode !== 0) return yield* new BashFailed({
|
|
5526
|
-
exitCode: result.exitCode,
|
|
5527
|
-
...present
|
|
5528
|
-
});
|
|
5529
7634
|
return {
|
|
5530
|
-
|
|
5531
|
-
...
|
|
7635
|
+
type: "image",
|
|
7636
|
+
note: [`Read image file [${inline$1.mimeType}]`, ...inline$1.hints].join("\n"),
|
|
7637
|
+
image: {
|
|
7638
|
+
data: inline$1.data,
|
|
7639
|
+
mimeType: inline$1.mimeType
|
|
7640
|
+
}
|
|
5532
7641
|
};
|
|
5533
7642
|
});
|
|
5534
|
-
|
|
5535
|
-
const
|
|
5536
|
-
const
|
|
5537
|
-
const
|
|
5538
|
-
const
|
|
5539
|
-
const
|
|
5540
|
-
|
|
5541
|
-
|
|
7643
|
+
const readText = (fs, resolved, params) => Effect.gen(function* () {
|
|
7644
|
+
const { path, offset } = params;
|
|
7645
|
+
const startLine = offset ? Math.max(0, Math.trunc(offset) - 1) : 0;
|
|
7646
|
+
const limit = params.limit === void 0 ? void 0 : Math.max(1, Math.trunc(params.limit));
|
|
7647
|
+
const endLine = startLine + Math.min(limit ?? 2e3, DEFAULT_MAX_LINES);
|
|
7648
|
+
const scan = yield* fs.scanLines(resolved, Number.isSafeInteger(endLine) ? {
|
|
7649
|
+
startLine,
|
|
7650
|
+
endLine,
|
|
7651
|
+
maxBytes: DEFAULT_MAX_BYTES
|
|
7652
|
+
} : {
|
|
7653
|
+
startLine: 0,
|
|
7654
|
+
endLine: 1,
|
|
7655
|
+
maxBytes: DEFAULT_MAX_BYTES
|
|
7656
|
+
});
|
|
7657
|
+
if (startLine >= Math.max(scan.totalLines, 1)) return yield* fail(`Offset ${offset} is beyond end of file (${scan.totalLines} lines total)`);
|
|
7658
|
+
const grown = Buffer.byteLength(scan.text, "utf8") > 51200 ? truncateHead(scan.text) : void 0;
|
|
7659
|
+
const firstLineBytes = grown?.firstLineExceedsLimit ? Buffer.byteLength(scan.text.split("\n", 1)[0] ?? "", "utf8") : scan.firstLineBytes;
|
|
7660
|
+
const shown = grown === void 0 ? scan : {
|
|
7661
|
+
text: grown.content,
|
|
7662
|
+
lines: grown.outputLines
|
|
7663
|
+
};
|
|
7664
|
+
const first = startLine + 1;
|
|
7665
|
+
const selected = Math.min(scan.totalLines, limit === void 0 ? Infinity : startLine + limit) - startLine;
|
|
7666
|
+
const truncation = (truncatedBy, firstLineExceedsLimit) => ({
|
|
7667
|
+
truncated: true,
|
|
7668
|
+
truncatedBy,
|
|
7669
|
+
totalLines: selected,
|
|
7670
|
+
outputLines: shown.lines,
|
|
7671
|
+
outputBytes: Buffer.byteLength(shown.text, "utf8"),
|
|
7672
|
+
firstLineExceedsLimit,
|
|
7673
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
7674
|
+
maxBytes: DEFAULT_MAX_BYTES
|
|
7675
|
+
});
|
|
7676
|
+
if (firstLineBytes > 51200) return {
|
|
7677
|
+
type: "text",
|
|
7678
|
+
text: `[Line ${first} is ${formatSize(firstLineBytes)}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${first}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`,
|
|
7679
|
+
truncation: {
|
|
7680
|
+
...truncation("bytes", true),
|
|
7681
|
+
outputLines: 0,
|
|
7682
|
+
outputBytes: 0
|
|
7683
|
+
}
|
|
7684
|
+
};
|
|
7685
|
+
if (shown.lines < selected) {
|
|
7686
|
+
const last = startLine + shown.lines;
|
|
7687
|
+
const truncatedBy = shown.lines < Math.min(selected, 2e3) ? "bytes" : "lines";
|
|
7688
|
+
const limitText = truncatedBy === "bytes" ? ` (${formatSize(DEFAULT_MAX_BYTES)} limit)` : "";
|
|
7689
|
+
return {
|
|
5542
7690
|
type: "text",
|
|
5543
|
-
text:
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
if (
|
|
5548
|
-
const
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
timeoutSeconds: timedOutAfter,
|
|
5555
|
-
...present
|
|
5556
|
-
});
|
|
5557
|
-
const code = yield* Ref.get(exitCode);
|
|
5558
|
-
if (code === null || code !== 0) return yield* new BashFailed({
|
|
5559
|
-
exitCode: code ?? -1,
|
|
5560
|
-
...present
|
|
5561
|
-
});
|
|
7691
|
+
text: `${shown.text}\n\n[Showing lines ${first}-${last} of ${scan.totalLines}${limitText}. Use offset=${last + 1} to continue.]`,
|
|
7692
|
+
truncation: truncation(truncatedBy, false)
|
|
7693
|
+
};
|
|
7694
|
+
}
|
|
7695
|
+
if (limit !== void 0 && startLine + limit < scan.totalLines) {
|
|
7696
|
+
const remaining = scan.totalLines - (startLine + limit);
|
|
7697
|
+
return {
|
|
7698
|
+
type: "text",
|
|
7699
|
+
text: `${shown.text}\n\n[${remaining} more lines in file. Use offset=${startLine + limit + 1} to continue.]`
|
|
7700
|
+
};
|
|
7701
|
+
}
|
|
5562
7702
|
return {
|
|
5563
|
-
|
|
5564
|
-
|
|
7703
|
+
type: "text",
|
|
7704
|
+
text: shown.text
|
|
5565
7705
|
};
|
|
5566
|
-
}));
|
|
5567
|
-
const bashHandler = (params) => Effect.gen(function* () {
|
|
5568
|
-
const shell = yield* ToolShell;
|
|
5569
|
-
return shell.stream !== void 0 ? yield* runStreaming(shell.stream, params) : yield* runBuffered(shell, params);
|
|
5570
7706
|
});
|
|
5571
|
-
|
|
5572
|
-
const
|
|
5573
|
-
const
|
|
5574
|
-
|
|
7707
|
+
const readHandler = (params) => Effect.gen(function* () {
|
|
7708
|
+
const fs = yield* io_exports.SandboxIO.FileSystem;
|
|
7709
|
+
const resolved = yield* resolveRead(fs, params.path);
|
|
7710
|
+
const stat = yield* fs.stat(resolved);
|
|
7711
|
+
if (stat.isDirectory) return yield* fail("EISDIR: illegal operation on a directory, read");
|
|
7712
|
+
if (!stat.isFile) return yield* fail("Not a regular file");
|
|
7713
|
+
const head = yield* fs.readBytes(resolved, 0, BINARY_PROBE_BYTES);
|
|
7714
|
+
const mimeType = yield* detectOf({
|
|
7715
|
+
size: stat.size ?? Infinity,
|
|
7716
|
+
read: (offset, length) => offset + length <= head.length ? Effect.succeed(head.subarray(offset, offset + length)) : fs.readBytes(resolved, offset, length)
|
|
7717
|
+
});
|
|
7718
|
+
if (mimeType !== void 0) return yield* readImage(fs, resolved, mimeType);
|
|
7719
|
+
if (head.includes(0)) return yield* fail(`Unsupported read ${params.path}: binary file.`);
|
|
7720
|
+
return yield* readText(fs, resolved, params);
|
|
7721
|
+
}).pipe(Effect.catchTag("SandboxFileSystemError", fsFailure$1(params.path)));
|
|
7722
|
+
/** The read tool: definition + handler, wired the testable (def/exec split) way. */
|
|
7723
|
+
const readTool = tool_exports.implement(readDef, readHandler);
|
|
7724
|
+
const readPlugin = (0, plugin_exports$1.define)({
|
|
7725
|
+
id: "codework.tool.read",
|
|
5575
7726
|
kind: "tool",
|
|
5576
|
-
setup: Effect.fn("
|
|
5577
|
-
const
|
|
5578
|
-
|
|
5579
|
-
ctx.plugin.tools.add(tool_exports.provide(bashTool, mounted));
|
|
7727
|
+
setup: Effect.fn("ReadPlugin.setup")(function* (ctx) {
|
|
7728
|
+
const fs = yield* io_exports.SandboxIO.FileSystem;
|
|
7729
|
+
ctx.plugin.tools.add(tool_exports.provide(readTool, Layer.succeed(io_exports.SandboxIO.FileSystem, fs)));
|
|
5580
7730
|
})
|
|
5581
7731
|
});
|
|
5582
7732
|
//#endregion
|
|
5583
|
-
//#region src/plugin/builtin.ts
|
|
7733
|
+
//#region src/plugin/builtin/tool/write.ts
|
|
5584
7734
|
/**
|
|
5585
|
-
*
|
|
5586
|
-
*
|
|
7735
|
+
* The built-in write plugin. The whole write holds the file's mutation lock, so
|
|
7736
|
+
* it never interleaves with an `edit` of the same file; the filesystem creates
|
|
7737
|
+
* missing parent directories on every backend.
|
|
5587
7738
|
*/
|
|
5588
|
-
const
|
|
7739
|
+
const WriteParams = Schema.Struct({
|
|
7740
|
+
path: Schema.String.annotate({ description: "Path to the file to write (relative or absolute)" }),
|
|
7741
|
+
content: Schema.String.annotate({ description: "Content to write to the file" })
|
|
7742
|
+
});
|
|
7743
|
+
var WriteFailed = class extends Schema.TaggedError()("WriteFailed", { message: Schema.String }) {};
|
|
7744
|
+
const writeDef = tool_exports.define({
|
|
7745
|
+
name: "write",
|
|
7746
|
+
label: "write",
|
|
7747
|
+
description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
|
|
7748
|
+
promptSnippet: "Create or overwrite files",
|
|
7749
|
+
promptGuidelines: ["Use write only for new files or complete rewrites."],
|
|
7750
|
+
parameters: WriteParams,
|
|
7751
|
+
constrainedSampling: {
|
|
7752
|
+
type: "json_schema",
|
|
7753
|
+
strict: "prefer"
|
|
7754
|
+
},
|
|
7755
|
+
success: Schema.Struct({ path: Schema.String }),
|
|
7756
|
+
failure: WriteFailed,
|
|
7757
|
+
encodeContent: (success) => [{
|
|
7758
|
+
type: "text",
|
|
7759
|
+
text: `Successfully wrote to ${success.path}`
|
|
7760
|
+
}],
|
|
7761
|
+
encodeFailureContent: (failure) => [{
|
|
7762
|
+
type: "text",
|
|
7763
|
+
text: failure.message
|
|
7764
|
+
}],
|
|
7765
|
+
encodeDetails: () => void 0
|
|
7766
|
+
});
|
|
7767
|
+
/** Filesystem failures are the model's to see, as the backend reported them. */
|
|
7768
|
+
const fsFailure = (path) => (error) => Effect.fail(new WriteFailed({ message: error.cause instanceof Error ? error.cause.message : `Could not write file: ${path}` }));
|
|
7769
|
+
const writeHandler = (params) => Effect.gen(function* () {
|
|
7770
|
+
const fs = yield* io_exports.SandboxIO.FileSystem;
|
|
7771
|
+
const mutation = yield* io_exports.SandboxIO.Mutation;
|
|
7772
|
+
const path = normalize$1(params.path);
|
|
7773
|
+
yield* mutation.withFile(path, fs.writeFile(path, params.content));
|
|
7774
|
+
return { path: params.path };
|
|
7775
|
+
}).pipe(Effect.catchTag("SandboxFileSystemError", fsFailure(params.path)));
|
|
7776
|
+
/** The write tool: definition + handler, wired the testable (def/exec split) way. */
|
|
7777
|
+
const writeTool = tool_exports.implement(writeDef, writeHandler);
|
|
7778
|
+
const writePlugin = (0, plugin_exports$1.define)({
|
|
7779
|
+
id: "codework.tool.write",
|
|
7780
|
+
kind: "tool",
|
|
7781
|
+
setup: Effect.fn("WritePlugin.setup")(function* (ctx) {
|
|
7782
|
+
const mounted = Layer.merge(Layer.succeed(io_exports.SandboxIO.FileSystem, yield* io_exports.SandboxIO.FileSystem), Layer.succeed(io_exports.SandboxIO.Mutation, yield* io_exports.SandboxIO.Mutation));
|
|
7783
|
+
ctx.plugin.tools.add(tool_exports.provide(writeTool, mounted));
|
|
7784
|
+
})
|
|
7785
|
+
});
|
|
5589
7786
|
//#endregion
|
|
5590
|
-
//#region src/
|
|
7787
|
+
//#region src/plugin/builtin.ts
|
|
5591
7788
|
/**
|
|
5592
|
-
*
|
|
5593
|
-
*
|
|
5594
|
-
*
|
|
7789
|
+
* Every plugin the harness ships, and the selection a caller who passes no `plugins` gets. Tool
|
|
7790
|
+
* contributors come first; within the prompt domain, declaration order is run order. The search
|
|
7791
|
+
* tools (grep, find, ls) are `optIn`, as Pi ships them off: settings turn each on with
|
|
7792
|
+
* `{ "plugin": "codework.tool.grep", "enabled": true }`.
|
|
5595
7793
|
*/
|
|
5596
|
-
const
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
if (value === null || value === void 0) continue;
|
|
5608
|
-
const previous = result.get(key);
|
|
5609
|
-
result.set(key, isRecord$1(previous) && isRecord$1(value) ? merge(previous, value) : copy(value));
|
|
5610
|
-
}
|
|
5611
|
-
}
|
|
5612
|
-
return Object.fromEntries(result);
|
|
5613
|
-
}
|
|
5614
|
-
/** Normalize nullable properties before schema validation, without changing array positions. */
|
|
5615
|
-
const normalize = copy;
|
|
7794
|
+
const builtins = Object.freeze([
|
|
7795
|
+
bashPlugin,
|
|
7796
|
+
readPlugin,
|
|
7797
|
+
writePlugin,
|
|
7798
|
+
editPlugin,
|
|
7799
|
+
grepPlugin,
|
|
7800
|
+
findPlugin,
|
|
7801
|
+
lsPlugin,
|
|
7802
|
+
defaultPromptPlugin,
|
|
7803
|
+
instructionPlugin
|
|
7804
|
+
]);
|
|
5616
7805
|
//#endregion
|
|
5617
7806
|
//#region src/util/module.ts
|
|
5618
7807
|
/*!
|
|
@@ -6169,7 +8358,7 @@ const isPath = (reference) => reference.startsWith("file:") || reference.startsW
|
|
|
6169
8358
|
* - a **CLI argument** anchors to the process's startup directory, because that is where the
|
|
6170
8359
|
* person typed it.
|
|
6171
8360
|
*/
|
|
6172
|
-
const parse$
|
|
8361
|
+
const parse$1 = (reference, from) => Effect.gen(function* () {
|
|
6173
8362
|
const fail = (reason, message) => new SourceError({
|
|
6174
8363
|
reason,
|
|
6175
8364
|
reference,
|
|
@@ -6386,7 +8575,7 @@ const slugOf = (target) => target.kind === "git" ? target.slug : canonical$1(tar
|
|
|
6386
8575
|
*/
|
|
6387
8576
|
const digest = Effect.fn("PluginStore.digest")(function* (target, cache, from) {
|
|
6388
8577
|
const source = target.kind === "registry" ? `${yield* registry(target, cache, from)}\0${target.spec}` : target.spec;
|
|
6389
|
-
return
|
|
8578
|
+
return Hex.encode(yield* crypto.digest("SHA-256", new TextEncoder().encode(source)));
|
|
6390
8579
|
});
|
|
6391
8580
|
const entryDir = Effect.fn("PluginStore.entryDir")(function* (target, cache, from) {
|
|
6392
8581
|
const key = yield* digest(target, cache, from);
|
|
@@ -6691,6 +8880,7 @@ const Definition = Schema.Struct({
|
|
|
6691
8880
|
id: Id,
|
|
6692
8881
|
kind: Schema.Literals(Object.keys(plugin_exports$1.domains)),
|
|
6693
8882
|
events: Schema.optional(Schema.Array(Schema.declare((value) => Predicate.hasProperty(value, "type") && Predicate.isString(value.type)))),
|
|
8883
|
+
optIn: Schema.optional(Schema.Boolean),
|
|
6694
8884
|
setup: Schema.declare((value) => Predicate.isFunction(value))
|
|
6695
8885
|
});
|
|
6696
8886
|
/** Every plugin failure says which file to go and edit, when there is one. */
|
|
@@ -6790,7 +8980,7 @@ const localUrl = Effect.fn("PluginLoader.localUrl")(function* (location, origin)
|
|
|
6790
8980
|
*/
|
|
6791
8981
|
const install = Effect.fn("PluginLoader.install")(function* (target, cache, origin, options) {
|
|
6792
8982
|
const added = yield* add(target, cache, {
|
|
6793
|
-
from: anchor
|
|
8983
|
+
from: anchor(origin.file, options.hostDir),
|
|
6794
8984
|
validate: (fetched) => imported(url(fetched.entrypoint), origin, options.import)
|
|
6795
8985
|
});
|
|
6796
8986
|
return {
|
|
@@ -6806,13 +8996,13 @@ const install = Effect.fn("PluginLoader.install")(function* (target, cache, orig
|
|
|
6806
8996
|
* package boundary discovered upward from the chain's start -- the root itself -- so anchoring at
|
|
6807
8997
|
* the file's own directory would miss it. Absent a declaring file the host context stands in.
|
|
6808
8998
|
*/
|
|
6809
|
-
const anchor
|
|
8999
|
+
const anchor = (file, fallback) => {
|
|
6810
9000
|
if (file === void 0) return fallback;
|
|
6811
9001
|
const directory = hostPath.dirname(file);
|
|
6812
9002
|
return hostPath.basename(directory) === ".codework" ? hostPath.dirname(directory) : directory;
|
|
6813
9003
|
};
|
|
6814
9004
|
/** The default installer, or the one the caller injected. */
|
|
6815
|
-
const installer = (target, origin, options) => options.install === void 0 ? install(target, options.cache, origin, options) : options.install(target, options.cache, anchor
|
|
9005
|
+
const installer = (target, origin, options) => options.install === void 0 ? install(target, options.cache, origin, options) : options.install(target, options.cache, anchor(origin.file, options.hostDir));
|
|
6816
9006
|
/** Import a module and confirm it default-exports a plugin. */
|
|
6817
9007
|
const imported = Effect.fn("PluginLoader.imported")(function* (url, origin, load) {
|
|
6818
9008
|
const module = yield* Effect.tryPromise({
|
|
@@ -6844,15 +9034,15 @@ const inspect = Effect.fn("PluginLoader.inspect")(function* (reference, options)
|
|
|
6844
9034
|
reference,
|
|
6845
9035
|
file: options.file
|
|
6846
9036
|
};
|
|
6847
|
-
const target = yield* parse$
|
|
6848
|
-
const loaded = yield* load$
|
|
9037
|
+
const target = yield* parse$1(reference, options.hostDir);
|
|
9038
|
+
const loaded = yield* load$2(target, origin, options);
|
|
6849
9039
|
return {
|
|
6850
9040
|
id: loaded.plugin.id,
|
|
6851
9041
|
...loaded.version === void 0 ? {} : { version: loaded.version },
|
|
6852
9042
|
...loaded.name === void 0 ? {} : { name: loaded.name }
|
|
6853
9043
|
};
|
|
6854
9044
|
});
|
|
6855
|
-
const load$
|
|
9045
|
+
const load$2 = Effect.fn("PluginLoader.load")(function* (target, origin, options) {
|
|
6856
9046
|
const installed = yield* target.kind === "local" ? localUrl(target.path, origin).pipe(Effect.mapError((cause) => sourceFailure(origin, cause))) : installer(target, origin, options).pipe(Effect.mapError((cause) => origin.file === void 0 ? cause : declaredIn(cause, origin.file)));
|
|
6857
9047
|
const address = target.kind === "local" && options.reload !== void 0 && options.reload > 0 ? `${installed.url}?reload=${options.reload}` : installed.url;
|
|
6858
9048
|
return {
|
|
@@ -6870,7 +9060,7 @@ const load$3 = Effect.fn("PluginLoader.load")(function* (target, origin, options
|
|
|
6870
9060
|
* are one package, and `./plugins/x.ts` is the file it resolves to from the directory that
|
|
6871
9061
|
* declared it. Pure -- nothing is installed, imported or read.
|
|
6872
9062
|
*/
|
|
6873
|
-
const canonical = (reference, hostDir) => Effect.map(parse$
|
|
9063
|
+
const canonical = (reference, hostDir) => Effect.map(parse$1(reference, hostDir), canonical$1);
|
|
6874
9064
|
//#endregion
|
|
6875
9065
|
//#region src/plugin/catalog.ts
|
|
6876
9066
|
/**
|
|
@@ -6897,7 +9087,7 @@ const isPatch = (reference) => Predicate.isObject(reference) && !Predicate.isFun
|
|
|
6897
9087
|
*
|
|
6898
9088
|
* Loading is keyed by what a reference resolves to, so naming one module twice imports it once.
|
|
6899
9089
|
*/
|
|
6900
|
-
const load$
|
|
9090
|
+
const load$1 = Effect.fn("PluginCatalog.load")(function* (references, options) {
|
|
6901
9091
|
const plugins = /* @__PURE__ */ new Map();
|
|
6902
9092
|
const aliases = /* @__PURE__ */ new Map();
|
|
6903
9093
|
const versions = /* @__PURE__ */ new Map();
|
|
@@ -6932,14 +9122,14 @@ const load$2 = Effect.fn("PluginCatalog.load")(function* (references, options) {
|
|
|
6932
9122
|
remember(plugin, []);
|
|
6933
9123
|
continue;
|
|
6934
9124
|
}
|
|
6935
|
-
const target = yield* parse$
|
|
9125
|
+
const target = yield* parse$1(reference, options.hostDir);
|
|
6936
9126
|
const key = target.kind === "local" ? target.path : target.spec;
|
|
6937
9127
|
const already = seen.get(key);
|
|
6938
9128
|
if (already !== void 0) {
|
|
6939
9129
|
aliases.set(reference, already);
|
|
6940
9130
|
continue;
|
|
6941
9131
|
}
|
|
6942
|
-
const definition = yield* load$
|
|
9132
|
+
const definition = yield* load$2(target, origin, options);
|
|
6943
9133
|
yield* note(definition.plugin, definition.version === void 0 ? key : `${key}@${definition.version}`);
|
|
6944
9134
|
const names = target.kind === "local" ? [target.path, ...definition.name === void 0 ? [] : [definition.name]] : [canonical$1(target), target.spec];
|
|
6945
9135
|
remember(definition.plugin, [reference, ...names], {
|
|
@@ -6976,11 +9166,14 @@ const load$2 = Effect.fn("PluginCatalog.load")(function* (references, options) {
|
|
|
6976
9166
|
const select = Effect.fn("PluginCatalog.select")(function* (references, pool) {
|
|
6977
9167
|
const operations = /* @__PURE__ */ new Map();
|
|
6978
9168
|
const missing = [];
|
|
6979
|
-
const choose = (id, index) =>
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
9169
|
+
const choose = (id, index) => {
|
|
9170
|
+
const previous = operations.get(id);
|
|
9171
|
+
operations.set(id, {
|
|
9172
|
+
enabled: pool.plugins.get(id)?.optIn === true ? previous?.enabled ?? false : true,
|
|
9173
|
+
index,
|
|
9174
|
+
options: previous?.options ?? {}
|
|
9175
|
+
});
|
|
9176
|
+
};
|
|
6984
9177
|
for (const [index, reference] of references.entries()) {
|
|
6985
9178
|
if (isPatch(reference)) {
|
|
6986
9179
|
const named = reference.plugin ?? reference.package;
|
|
@@ -7061,12 +9254,12 @@ const follow = Effect.fn("PluginCatalog.follow")(function* (references, pool, op
|
|
|
7061
9254
|
for (const reference of references) {
|
|
7062
9255
|
if (typeof reference !== "string") continue;
|
|
7063
9256
|
const owner = options.declared?.get(reference);
|
|
7064
|
-
const from = anchor
|
|
9257
|
+
const from = anchor(owner, options.hostDir);
|
|
7065
9258
|
const id = pool.aliases.get(reference);
|
|
7066
9259
|
if (id !== void 0) {
|
|
7067
9260
|
const origin = pool.origins.get(id);
|
|
7068
9261
|
if (owner === void 0 || origin?.reference !== reference) continue;
|
|
7069
|
-
if (anchor
|
|
9262
|
+
if (anchor(origin.file, options.hostDir) === from) continue;
|
|
7070
9263
|
}
|
|
7071
9264
|
if (Option.isSome(yield* filed(reference, from))) {
|
|
7072
9265
|
moved = true;
|
|
@@ -7075,7 +9268,7 @@ const follow = Effect.fn("PluginCatalog.follow")(function* (references, pool, op
|
|
|
7075
9268
|
}
|
|
7076
9269
|
if (!moved) for (const origin of pool.origins.values()) {
|
|
7077
9270
|
if (origin.generation === void 0) continue;
|
|
7078
|
-
const current = yield* filed(origin.reference, anchor
|
|
9271
|
+
const current = yield* filed(origin.reference, anchor(origin.file, options.hostDir));
|
|
7079
9272
|
if (Option.isSome(current) && current.value.generation !== void 0 && current.value.generation > origin.generation) {
|
|
7080
9273
|
moved = true;
|
|
7081
9274
|
break;
|
|
@@ -7084,7 +9277,7 @@ const follow = Effect.fn("PluginCatalog.follow")(function* (references, pool, op
|
|
|
7084
9277
|
if (!moved) return Option.none();
|
|
7085
9278
|
const retained = [];
|
|
7086
9279
|
for (const origin of pool.origins.values()) {
|
|
7087
|
-
if (references.includes(origin.reference) || Option.isSome(yield* filed(origin.reference, anchor
|
|
9280
|
+
if (references.includes(origin.reference) || Option.isSome(yield* filed(origin.reference, anchor(origin.file, options.hostDir)))) {
|
|
7088
9281
|
retained.push(origin);
|
|
7089
9282
|
continue;
|
|
7090
9283
|
}
|
|
@@ -7094,13 +9287,13 @@ const follow = Effect.fn("PluginCatalog.follow")(function* (references, pool, op
|
|
|
7094
9287
|
const known = /* @__PURE__ */ new Map();
|
|
7095
9288
|
for (const origin of retained) if (origin.file !== void 0) known.set(origin.reference, origin.file);
|
|
7096
9289
|
for (const [reference, file] of options.declared ?? /* @__PURE__ */ new Map()) known.set(reference, file);
|
|
7097
|
-
return Option.some(yield* load$
|
|
9290
|
+
return Option.some(yield* load$1(accumulated, {
|
|
7098
9291
|
...options,
|
|
7099
9292
|
declared: known
|
|
7100
9293
|
}));
|
|
7101
9294
|
});
|
|
7102
9295
|
Effect.fn("PluginCatalog.prepare")(function* (references, options) {
|
|
7103
|
-
const pool = yield* load$
|
|
9296
|
+
const pool = yield* load$1(references, options);
|
|
7104
9297
|
return (yield* select(references, pool)).selection;
|
|
7105
9298
|
});
|
|
7106
9299
|
//#endregion
|
|
@@ -7178,7 +9371,7 @@ const make$7 = (options) => Effect.gen(function* () {
|
|
|
7178
9371
|
});
|
|
7179
9372
|
//#endregion
|
|
7180
9373
|
//#region src/runner/execute.ts
|
|
7181
|
-
const layer$
|
|
9374
|
+
const layer$6 = Layer.effect(Service$6, Effect.gen(function* () {
|
|
7182
9375
|
const store = yield* Service$14;
|
|
7183
9376
|
const sandbox = yield* Controller;
|
|
7184
9377
|
const sql = yield* SqlClient.SqlClient;
|
|
@@ -7244,11 +9437,6 @@ const layer$7 = Layer.effect(Service$6, Effect.gen(function* () {
|
|
|
7244
9437
|
});
|
|
7245
9438
|
}));
|
|
7246
9439
|
//#endregion
|
|
7247
|
-
//#region src/settings/schema.ts
|
|
7248
|
-
var schema_exports$3 = /* @__PURE__ */ __exportAll({});
|
|
7249
|
-
import * as import__codeworksh_plugin_settings from "@codeworksh/plugin/settings";
|
|
7250
|
-
__reExport(schema_exports$3, import__codeworksh_plugin_settings);
|
|
7251
|
-
//#endregion
|
|
7252
9440
|
//#region src/settings/resolve.ts
|
|
7253
9441
|
const patternMatches = (pattern, id) => new RegExp(`^${pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`).test(id);
|
|
7254
9442
|
function collect(settings, provider, id) {
|
|
@@ -7287,11 +9475,12 @@ function resolveRequest(block, model) {
|
|
|
7287
9475
|
"toolExecution",
|
|
7288
9476
|
"contextWindow",
|
|
7289
9477
|
"protocol",
|
|
9478
|
+
"apiKey",
|
|
7290
9479
|
"extras"
|
|
7291
9480
|
].includes(key)));
|
|
7292
9481
|
return {
|
|
7293
9482
|
...resolveOptions(block),
|
|
7294
|
-
...Object.keys(bag).length === 0 ? {} : { providerOptions: { [Model
|
|
9483
|
+
...Object.keys(bag).length === 0 ? {} : { providerOptions: { [Model.optionsKey(model)]: bag } }
|
|
7295
9484
|
};
|
|
7296
9485
|
}
|
|
7297
9486
|
/**
|
|
@@ -7299,7 +9488,8 @@ function resolveRequest(block, model) {
|
|
|
7299
9488
|
*
|
|
7300
9489
|
* Separate from {@link resolveRequest} because these steer the loop itself -- how hard to
|
|
7301
9490
|
* think, how to schedule tool calls -- rather than the request body. Runtime bindings
|
|
7302
|
-
* outrank a matched block, which outranks the file's top level.
|
|
9491
|
+
* outrank a matched block, which outranks the file's top level. The thinking level is the
|
|
9492
|
+
* requested one; State clamps it to the model once the catalog lookup is in hand.
|
|
7303
9493
|
*/
|
|
7304
9494
|
const resolveControls = (settings, block, runtime) => ({
|
|
7305
9495
|
thinkingLevel: runtime.thinkingLevel ?? block.thinkingLevel ?? settings.model.thinkingLevel,
|
|
@@ -7320,6 +9510,25 @@ function compose(settings, options = {}, session = {}) {
|
|
|
7320
9510
|
};
|
|
7321
9511
|
}
|
|
7322
9512
|
//#endregion
|
|
9513
|
+
//#region src/runner/attribution.ts
|
|
9514
|
+
const OPENROUTER_HOST = "openrouter.ai";
|
|
9515
|
+
const isOpenRouter = (model) => model.provider.id === "openrouter" || (model.api?.url ?? model.baseUrl).includes(OPENROUTER_HOST);
|
|
9516
|
+
const defaults = (model) => isOpenRouter(model) ? {
|
|
9517
|
+
"HTTP-Referer": "https://codework.sh",
|
|
9518
|
+
"X-OpenRouter-Title": "CodeWork",
|
|
9519
|
+
"X-OpenRouter-Categories": "cli-agent,personal-agent"
|
|
9520
|
+
} : void 0;
|
|
9521
|
+
const withAttribution = (model) => {
|
|
9522
|
+
const headers = defaults(model);
|
|
9523
|
+
return headers === void 0 ? model : {
|
|
9524
|
+
...model,
|
|
9525
|
+
headers: {
|
|
9526
|
+
...headers,
|
|
9527
|
+
...model.headers
|
|
9528
|
+
}
|
|
9529
|
+
};
|
|
9530
|
+
};
|
|
9531
|
+
//#endregion
|
|
7323
9532
|
//#region src/runner/event.ts
|
|
7324
9533
|
const make$6 = Effect.fn("LLMEventPublisher.make")(function* (input) {
|
|
7325
9534
|
const events = yield* Service$16;
|
|
@@ -7525,15 +9734,27 @@ const messageFailure = (message) => {
|
|
|
7525
9734
|
};
|
|
7526
9735
|
/** Resolve once before exchange setup; execution reuses this exact instance. */
|
|
7527
9736
|
const resolve$1 = Effect.fn("LLM.resolve")(function* (input) {
|
|
7528
|
-
const
|
|
7529
|
-
|
|
9737
|
+
const overrides = resolveOverrides(input.settings ?? {});
|
|
9738
|
+
const declared = entry(input.models ?? [], input.provider, input.model);
|
|
9739
|
+
const model = declared !== void 0 ? {
|
|
9740
|
+
...declared,
|
|
9741
|
+
...overrides
|
|
9742
|
+
} : yield* Effect.tryPromise({
|
|
9743
|
+
try: () => llm(input.provider, input.model, overrides),
|
|
7530
9744
|
catch: (cause) => loadError(cause) ?? providerErrorFromUnknown(input, cause)
|
|
7531
9745
|
});
|
|
7532
9746
|
if (model === void 0) return yield* new ModelNotFoundError({
|
|
7533
9747
|
provider: input.provider,
|
|
7534
9748
|
model: input.model
|
|
7535
9749
|
});
|
|
7536
|
-
|
|
9750
|
+
const apiKey = input.settings?.apiKey;
|
|
9751
|
+
return apiKey === void 0 ? model : {
|
|
9752
|
+
...model,
|
|
9753
|
+
provider: {
|
|
9754
|
+
...model.provider,
|
|
9755
|
+
key: (0, schema_exports$3.keyName)(apiKey)
|
|
9756
|
+
}
|
|
9757
|
+
};
|
|
7537
9758
|
});
|
|
7538
9759
|
/**
|
|
7539
9760
|
* Fill in what an OAuth provider needs and the model catalog cannot carry: the
|
|
@@ -7542,7 +9763,7 @@ const resolve$1 = Effect.fn("LLM.resolve")(function* (input) {
|
|
|
7542
9763
|
* with no stored login resolves to `{}` so aikit reports the missing key.
|
|
7543
9764
|
*/
|
|
7544
9765
|
const oauthCredentials = async (protocol, options) => {
|
|
7545
|
-
const storage = options.authFile
|
|
9766
|
+
const storage = oauthStorage(options.authFile);
|
|
7546
9767
|
if (protocol === "openai-codex") {
|
|
7547
9768
|
const apiKey = await getOpenAICodexApiKey({ storage: new JsonOpenAICodexAuthStorage(storage) });
|
|
7548
9769
|
return apiKey === void 0 ? {} : { apiKey };
|
|
@@ -7562,7 +9783,7 @@ const oauthCredentials = async (protocol, options) => {
|
|
|
7562
9783
|
};
|
|
7563
9784
|
/** Start a provider stream using the model pinned by State. */
|
|
7564
9785
|
const openWith = (options = {}) => Effect.fn("LLM.open")(function* (input, signal) {
|
|
7565
|
-
const model = input.resolvedModel;
|
|
9786
|
+
const model = withAttribution(input.resolvedModel);
|
|
7566
9787
|
const configured = runtimeOptions(input, model, signal);
|
|
7567
9788
|
const resolved = configured.apiKey === void 0 ? yield* Effect.tryPromise({
|
|
7568
9789
|
try: () => oauthCredentials(model.protocol, options),
|
|
@@ -7695,8 +9916,9 @@ const encodeOutcome = (def, call, exit, latest) => Effect.gen(function* () {
|
|
|
7695
9916
|
if (Exit.isSuccess(exit)) {
|
|
7696
9917
|
const encoded = yield* Schema.encodeUnknownEffect(asCodec(def.success))(exit.value).pipe(Effect.orDie);
|
|
7697
9918
|
const content = def.encodeContent ? def.encodeContent(exit.value) : [yield* jsonText(encoded)];
|
|
9919
|
+
const details = def.encodeDetails ? def.encodeDetails(exit.value) : encoded;
|
|
7698
9920
|
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
7699
|
-
return completed(call, content, now,
|
|
9921
|
+
return completed(call, content, now, details);
|
|
7700
9922
|
}
|
|
7701
9923
|
const cause = exit.cause;
|
|
7702
9924
|
if (Cause.hasInterrupts(cause)) {
|
|
@@ -7761,6 +9983,15 @@ const make$4 = (tools) => {
|
|
|
7761
9983
|
impls.set(name, entry);
|
|
7762
9984
|
}
|
|
7763
9985
|
const wire = [...impls.values()].map(({ tool }) => (0, tool_exports.toAikitTool)(tool.definition));
|
|
9986
|
+
const decoders = new Map(wire.map((tool) => {
|
|
9987
|
+
const decode = Schema.decodeUnknownEffect(Schema.toCodecJson(asCodec(impls.get(tool.name).tool.definition.parameters)));
|
|
9988
|
+
const parse = (args) => {
|
|
9989
|
+
const normalized = structuredClone(args);
|
|
9990
|
+
normalizeOptionalNulls(normalized, tool.parameters);
|
|
9991
|
+
return decode(normalized);
|
|
9992
|
+
};
|
|
9993
|
+
return [tool.name, parse];
|
|
9994
|
+
}));
|
|
7764
9995
|
const handle = (call, options) => Effect.gen(function* () {
|
|
7765
9996
|
const entry = impls.get(call.name);
|
|
7766
9997
|
if (entry === void 0) {
|
|
@@ -7772,7 +10003,18 @@ const make$4 = (tools) => {
|
|
|
7772
10003
|
}
|
|
7773
10004
|
const { tool: impl, hooks } = entry;
|
|
7774
10005
|
const def = impl.definition;
|
|
7775
|
-
const
|
|
10006
|
+
const prepared = yield* Effect.result(Effect.try({
|
|
10007
|
+
try: () => def.prepareArguments ? def.prepareArguments(call.arguments) : call.arguments,
|
|
10008
|
+
catch: (cause) => cause instanceof Error ? cause.message : String(cause)
|
|
10009
|
+
}));
|
|
10010
|
+
if (Result.isFailure(prepared)) {
|
|
10011
|
+
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
10012
|
+
return errored(call, [text(`Invalid arguments for ${call.name}: ${prepared.failure}`)], now, {
|
|
10013
|
+
error: "invalid_arguments",
|
|
10014
|
+
name: call.name
|
|
10015
|
+
});
|
|
10016
|
+
}
|
|
10017
|
+
const decoded = yield* Effect.result(decoders.get(call.name)(prepared.success));
|
|
7776
10018
|
if (Result.isFailure(decoded)) {
|
|
7777
10019
|
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
7778
10020
|
return errored(call, [text(`Invalid arguments for ${call.name}: ${decoded.failure.message}`)], now, {
|
|
@@ -7915,20 +10157,109 @@ const make$3 = (tools) => {
|
|
|
7915
10157
|
Context.Service()("@codeworksh/harness/tool/registry/ToolRegistry");
|
|
7916
10158
|
const make$2 = () => {
|
|
7917
10159
|
let open = true;
|
|
7918
|
-
let
|
|
10160
|
+
let foundation;
|
|
10161
|
+
const sections = /* @__PURE__ */ new Map();
|
|
10162
|
+
const writable = () => {
|
|
10163
|
+
if (!open) throw new Error("prompt registry is closed");
|
|
10164
|
+
};
|
|
10165
|
+
const slot = (section) => {
|
|
10166
|
+
writable();
|
|
10167
|
+
const reason = Section.invalid(section.name);
|
|
10168
|
+
if (reason !== void 0) throw new Error(reason);
|
|
10169
|
+
if (!Section.isFormat(section.format)) throw new Error(`prompt section ${section.name} has an unknown format: ${JSON.stringify(section.format)}`);
|
|
10170
|
+
const builtin = Section.builtins.find((candidate) => candidate.name === section.name);
|
|
10171
|
+
if (builtin !== void 0 && builtin.format !== section.format) throw new Error(`prompt section ${section.name} is built in as "${builtin.format}"`);
|
|
10172
|
+
const existing = sections.get(section.name);
|
|
10173
|
+
if (existing === void 0) {
|
|
10174
|
+
const opened = {
|
|
10175
|
+
format: section.format,
|
|
10176
|
+
entries: []
|
|
10177
|
+
};
|
|
10178
|
+
sections.set(section.name, opened);
|
|
10179
|
+
return opened;
|
|
10180
|
+
}
|
|
10181
|
+
if (existing.format !== section.format) throw new Error(`prompt section ${section.name} is "${existing.format}"; a write as "${section.format}" cannot merge into it`);
|
|
10182
|
+
return existing;
|
|
10183
|
+
};
|
|
7919
10184
|
return {
|
|
7920
10185
|
registry: Object.freeze({
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
10186
|
+
foundation: Object.freeze({
|
|
10187
|
+
set: (text) => {
|
|
10188
|
+
writable();
|
|
10189
|
+
foundation = text;
|
|
10190
|
+
},
|
|
10191
|
+
get: () => foundation
|
|
10192
|
+
}),
|
|
10193
|
+
sections: Object.freeze({
|
|
10194
|
+
append: (section, entry) => {
|
|
10195
|
+
slot(section).entries.push(entry);
|
|
10196
|
+
},
|
|
10197
|
+
set: (section, entries) => {
|
|
10198
|
+
slot(section).entries = [...entries];
|
|
10199
|
+
},
|
|
10200
|
+
remove: (section) => {
|
|
10201
|
+
writable();
|
|
10202
|
+
sections.delete(section.name);
|
|
10203
|
+
},
|
|
10204
|
+
get: (section) => {
|
|
10205
|
+
const found = sections.get(section.name);
|
|
10206
|
+
return found === void 0 ? void 0 : Object.freeze([...found.entries]);
|
|
10207
|
+
}
|
|
10208
|
+
})
|
|
7926
10209
|
}),
|
|
7927
10210
|
close: () => {
|
|
7928
10211
|
open = false;
|
|
7929
10212
|
},
|
|
7930
|
-
|
|
10213
|
+
snapshot: () => ({
|
|
10214
|
+
foundation,
|
|
10215
|
+
sections: new Map(Array.from(sections, ([name, { format, entries }]) => [name, {
|
|
10216
|
+
format,
|
|
10217
|
+
entries: [...entries]
|
|
10218
|
+
}]))
|
|
10219
|
+
})
|
|
10220
|
+
};
|
|
10221
|
+
};
|
|
10222
|
+
//#endregion
|
|
10223
|
+
//#region src/plugin/prompt/render.ts
|
|
10224
|
+
const tag = (name, body) => `<${name}>\n${body}\n</${name}>`;
|
|
10225
|
+
/** Two spellings of one list entry dedupe against each other. */
|
|
10226
|
+
const normalize = (value) => value.trim().replace(/\s+/g, " ");
|
|
10227
|
+
const body = ({ format, entries }) => {
|
|
10228
|
+
if (format === "text") {
|
|
10229
|
+
const kept = entries.filter((entry) => entry.trim().length > 0);
|
|
10230
|
+
return kept.length === 0 ? void 0 : kept.join("\n\n");
|
|
10231
|
+
}
|
|
10232
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10233
|
+
for (const entry of entries) {
|
|
10234
|
+
const normalized = normalize(entry);
|
|
10235
|
+
if (normalized.length > 0) seen.add(normalized);
|
|
10236
|
+
}
|
|
10237
|
+
return seen.size === 0 ? void 0 : Array.from(seen, (line) => `- ${line}`).join("\n");
|
|
10238
|
+
};
|
|
10239
|
+
/** `(none)` rather than no section: a model told it has no tools behaves better than one left guessing. */
|
|
10240
|
+
const toolIndex = (tools) => {
|
|
10241
|
+
const listed = tools.filter((tool) => tool.promptSnippet !== void 0 && tool.promptSnippet.length > 0);
|
|
10242
|
+
if (listed.length === 0) return "(none)";
|
|
10243
|
+
return listed.map((tool) => `- ${tool.name}: ${tool.promptSnippet}`).join("\n");
|
|
10244
|
+
};
|
|
10245
|
+
const render = (input) => {
|
|
10246
|
+
const written = new Map(input.prompt.sections);
|
|
10247
|
+
const guidelines = input.tools.flatMap((tool) => tool.promptGuidelines ?? []);
|
|
10248
|
+
written.set(Section.Rules.name, {
|
|
10249
|
+
format: Section.Rules.format,
|
|
10250
|
+
entries: [...guidelines, ...written.get(Section.Rules.name)?.entries ?? []]
|
|
10251
|
+
});
|
|
10252
|
+
const parts = [input.prompt.foundation ?? "You are an expert coding assistant operating inside codework, a coding agent harness.", tag("tools", toolIndex(input.tools))];
|
|
10253
|
+
const push = (name) => {
|
|
10254
|
+
const section = written.get(name);
|
|
10255
|
+
const rendered = section === void 0 ? void 0 : body(section);
|
|
10256
|
+
if (rendered !== void 0) parts.push(tag(name, rendered));
|
|
10257
|
+
written.delete(name);
|
|
7931
10258
|
};
|
|
10259
|
+
for (const section of Section.builtins) push(section.name);
|
|
10260
|
+
parts.push(tag("cwd", input.directory));
|
|
10261
|
+
for (const name of Array.from(written.keys())) push(name);
|
|
10262
|
+
return parts.join("\n\n");
|
|
7932
10263
|
};
|
|
7933
10264
|
//#endregion
|
|
7934
10265
|
//#region src/plugin/tool/registry.ts
|
|
@@ -7954,6 +10285,8 @@ const assertTool = (tool) => {
|
|
|
7954
10285
|
if (def.failure !== void 0 && !Schema.isSchema(def.failure)) throw new Error(`Tool ${name}: failure must be an Effect Schema`);
|
|
7955
10286
|
if (def.encodeContent !== void 0 && !Predicate.isFunction(def.encodeContent)) throw new Error(`Tool ${name}: encodeContent must be a function`);
|
|
7956
10287
|
if (def.encodeFailureContent !== void 0 && !Predicate.isFunction(def.encodeFailureContent)) throw new Error(`Tool ${name}: encodeFailureContent must be a function`);
|
|
10288
|
+
if (def.prepareArguments !== void 0 && !Predicate.isFunction(def.prepareArguments)) throw new Error(`Tool ${name}: prepareArguments must be a function`);
|
|
10289
|
+
if (def.encodeDetails !== void 0 && !Predicate.isFunction(def.encodeDetails)) throw new Error(`Tool ${name}: encodeDetails must be a function`);
|
|
7957
10290
|
if (!Predicate.isFunction(tool.handler)) throw new Error(`Tool ${name}: handler must be a function`);
|
|
7958
10291
|
};
|
|
7959
10292
|
const assertHooks = (name, hooks) => {
|
|
@@ -8020,11 +10353,16 @@ const make = () => {
|
|
|
8020
10353
|
prompt: prompt.registry
|
|
8021
10354
|
}),
|
|
8022
10355
|
close,
|
|
8023
|
-
freeze: () => {
|
|
10356
|
+
freeze: (directory) => {
|
|
8024
10357
|
close();
|
|
10358
|
+
const resolved = make$3(tools.entries()).resolve();
|
|
8025
10359
|
return {
|
|
8026
|
-
tools:
|
|
8027
|
-
systemPrompt:
|
|
10360
|
+
tools: resolved,
|
|
10361
|
+
systemPrompt: render({
|
|
10362
|
+
prompt: prompt.snapshot(),
|
|
10363
|
+
tools: resolved.defs,
|
|
10364
|
+
directory
|
|
10365
|
+
})
|
|
8028
10366
|
};
|
|
8029
10367
|
}
|
|
8030
10368
|
};
|
|
@@ -8069,7 +10407,7 @@ const run = Effect.fn("PluginHost.run")(function* (selection, input) {
|
|
|
8069
10407
|
}));
|
|
8070
10408
|
}));
|
|
8071
10409
|
return yield* Effect.try({
|
|
8072
|
-
try: buckets.freeze,
|
|
10410
|
+
try: () => buckets.freeze(input.location.directory),
|
|
8073
10411
|
catch: (cause) => new SetupError({
|
|
8074
10412
|
message: `plugin snapshot freeze failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
8075
10413
|
cause
|
|
@@ -8079,8 +10417,8 @@ const run = Effect.fn("PluginHost.run")(function* (selection, input) {
|
|
|
8079
10417
|
});
|
|
8080
10418
|
//#endregion
|
|
8081
10419
|
//#region src/session/runtime.ts
|
|
8082
|
-
var Service$
|
|
8083
|
-
const layer$
|
|
10420
|
+
var Service$2 = class extends Context.Service()("@codeworksh/harness/session/runtime/Service") {};
|
|
10421
|
+
const layer$5 = Layer.effect(Service$2, Effect.gen(function* () {
|
|
8084
10422
|
const bindings = yield* Ref.make(/* @__PURE__ */ new Map());
|
|
8085
10423
|
const get = (sessionId) => Ref.get(bindings).pipe(Effect.map((entries) => Option.fromUndefinedOr(entries.get(sessionId))));
|
|
8086
10424
|
const set = (sessionId, value) => Ref.update(bindings, (entries) => {
|
|
@@ -8101,7 +10439,7 @@ const layer$6 = Layer.effect(Service$3, Effect.gen(function* () {
|
|
|
8101
10439
|
next.delete(sessionId);
|
|
8102
10440
|
return next;
|
|
8103
10441
|
});
|
|
8104
|
-
return Service$
|
|
10442
|
+
return Service$2.of({
|
|
8105
10443
|
get,
|
|
8106
10444
|
set,
|
|
8107
10445
|
update,
|
|
@@ -8109,202 +10447,6 @@ const layer$6 = Layer.effect(Service$3, Effect.gen(function* () {
|
|
|
8109
10447
|
});
|
|
8110
10448
|
}));
|
|
8111
10449
|
//#endregion
|
|
8112
|
-
//#region src/settings/settings.ts
|
|
8113
|
-
var settings_exports = /* @__PURE__ */ __exportAll({
|
|
8114
|
-
Service: () => Service$2,
|
|
8115
|
-
Settings: () => settings_exports,
|
|
8116
|
-
SettingsError: () => SettingsError,
|
|
8117
|
-
defaults: () => schema_exports$3.defaults,
|
|
8118
|
-
layer: () => layer$5,
|
|
8119
|
-
load: () => load$1,
|
|
8120
|
-
modules: () => modules,
|
|
8121
|
-
parse: () => parse$1,
|
|
8122
|
-
paths: () => paths,
|
|
8123
|
-
projectRoot: () => projectRoot,
|
|
8124
|
-
userConfigDir: () => userConfigDir
|
|
8125
|
-
});
|
|
8126
|
-
/**
|
|
8127
|
-
* An app-level `--user-config-dir` as the absolute directory it names: `~` is the user's home, and
|
|
8128
|
-
* a relative spelling is relative to `hostCwd` -- never to a session's `hostDir`, which is not
|
|
8129
|
-
* where the flag was typed.
|
|
8130
|
-
*/
|
|
8131
|
-
const userConfigDir = (raw, hostCwd) => hostPath.resolve(hostCwd, expandTilde(raw, hostPath));
|
|
8132
|
-
/** The settings files a directory may hold, in the order it is searched. */
|
|
8133
|
-
const settingsIn = (directory) => [hostPath.join(directory, "settings.jsonc"), hostPath.join(directory, "settings.json")];
|
|
8134
|
-
/** A directory and every ancestor above it, nearest first. */
|
|
8135
|
-
const ancestors = (from) => {
|
|
8136
|
-
const chain = [];
|
|
8137
|
-
for (let directory = from;; directory = hostPath.dirname(directory)) {
|
|
8138
|
-
chain.push(directory);
|
|
8139
|
-
if (hostPath.dirname(directory) === directory) return chain;
|
|
8140
|
-
}
|
|
8141
|
-
};
|
|
8142
|
-
const isDirectory = (path) => fileSystem.stat(path).pipe(Effect.map((info) => info.type === "Directory"), Effect.orElseSucceed(() => false));
|
|
8143
|
-
/**
|
|
8144
|
-
* The project a directory is in: the nearest ancestor holding a `.codework` **directory**.
|
|
8145
|
-
*
|
|
8146
|
-
* This is git's rule. `~/workspace/app/.codework` makes `~/workspace/app` a project, and
|
|
8147
|
-
* `~/workspace/app/packages/fubar` is inside it. Nearest wins outright -- the walk stops at the
|
|
8148
|
-
* first marker, so an inner project is never merged with an outer one.
|
|
8149
|
-
*
|
|
8150
|
-
* The marker is the directory, not a file in it. An empty `.codework/` is still a project root,
|
|
8151
|
-
* which is the point: creating the directory is how a person says "a project begins here", and it
|
|
8152
|
-
* has to mean that before any settings exist. It also makes the question one `stat` per ancestor
|
|
8153
|
-
* whose answer does not change as files come and go inside.
|
|
8154
|
-
*
|
|
8155
|
-
* `<home>` is skipped. `~/.codework` is the user layer, not a project; without this, running
|
|
8156
|
-
* anywhere under `$HOME` would find it as one and read the user layer twice -- and since plugin
|
|
8157
|
-
* entries accumulate across layers, every user plugin would load twice.
|
|
8158
|
-
*/
|
|
8159
|
-
const projectRoot = Effect.fn("Settings.projectRoot")(function* (from, home) {
|
|
8160
|
-
const userConfig = hostPath.resolve(home);
|
|
8161
|
-
for (const directory of ancestors(hostPath.resolve(from))) {
|
|
8162
|
-
const marker = hostPath.join(directory, appConfigDir);
|
|
8163
|
-
if (hostPath.resolve(marker) === userConfig) continue;
|
|
8164
|
-
if (yield* isDirectory(marker)) return directory;
|
|
8165
|
-
}
|
|
8166
|
-
});
|
|
8167
|
-
/**
|
|
8168
|
-
* Ordered layers, each a group of candidates where the first file that exists is selected -- a
|
|
8169
|
-
* group never contributes more than one file.
|
|
8170
|
-
*
|
|
8171
|
-
* `.jsonc` is the canonical spelling: these files are written by hand and read as JSONC, so the
|
|
8172
|
-
* extension says what the file is and an editor validates it as such. `.json` remains a fallback
|
|
8173
|
-
* everywhere, tried second, because the parser reads either and a file already named that must
|
|
8174
|
-
* keep working.
|
|
8175
|
-
*
|
|
8176
|
-
* `root` is an already-discovered project root ({@link projectRoot}), not a directory to search
|
|
8177
|
-
* from: the walk is a `stat` per ancestor and belongs with the other I/O. `undefined` contributes
|
|
8178
|
-
* no project layer at all, which is what a session with no host directory gets.
|
|
8179
|
-
*/
|
|
8180
|
-
function paths(input) {
|
|
8181
|
-
return [settingsIn(input.userConfigDir ?? input.home), input.root === void 0 ? [] : settingsIn(hostPath.join(input.root, appConfigDir))];
|
|
8182
|
-
}
|
|
8183
|
-
var SettingsError = class extends Schema.TaggedError()("SettingsError", {
|
|
8184
|
-
path: Schema.String,
|
|
8185
|
-
reason: Schema.Literals([
|
|
8186
|
-
"read",
|
|
8187
|
-
"parse",
|
|
8188
|
-
"decode"
|
|
8189
|
-
]),
|
|
8190
|
-
detail: Schema.String
|
|
8191
|
-
}) {
|
|
8192
|
-
get message() {
|
|
8193
|
-
return `${this.path}: ${this.reason}: ${this.detail}`;
|
|
8194
|
-
}
|
|
8195
|
-
};
|
|
8196
|
-
/** `line:column`, 1-based, for a parser offset into the source. */
|
|
8197
|
-
const at = (source, offset) => {
|
|
8198
|
-
const before = source.slice(0, offset).split("\n");
|
|
8199
|
-
return `${before.length}:${(before.at(-1)?.length ?? 0) + 1}`;
|
|
8200
|
-
};
|
|
8201
|
-
const parse$1 = Effect.fn("Settings.parse")(function* (path, source) {
|
|
8202
|
-
const errors = [];
|
|
8203
|
-
const json = parse(source, errors, {
|
|
8204
|
-
allowTrailingComma: true,
|
|
8205
|
-
disallowComments: false
|
|
8206
|
-
});
|
|
8207
|
-
const first = errors[0];
|
|
8208
|
-
if (first !== void 0) return yield* new SettingsError({
|
|
8209
|
-
path,
|
|
8210
|
-
reason: "parse",
|
|
8211
|
-
detail: `${printParseErrorCode(first.error)} at ${at(source, first.offset)}`
|
|
8212
|
-
});
|
|
8213
|
-
const document = normalize(json);
|
|
8214
|
-
if (Predicate.isObject(document) && Predicate.isObject(json) && "plugins" in json) Object.assign(document, { plugins: json.plugins });
|
|
8215
|
-
return yield* Schema.decodeUnknownEffect(schema_exports$3.Patch)(document).pipe(Effect.mapError((error) => new SettingsError({
|
|
8216
|
-
path,
|
|
8217
|
-
reason: "decode",
|
|
8218
|
-
detail: SchemaIssue.makeFormatterStandardSchemaV1()(error.issue).issues.map((issue) => `${issue.path?.map((part) => typeof part === "object" ? String(part.key) : part).join(".") ?? "settings"}: invalid value`).join("; ")
|
|
8219
|
-
})));
|
|
8220
|
-
});
|
|
8221
|
-
var Service$2 = class extends Context.Service()("@codeworksh/harness/settings/settings/Service") {};
|
|
8222
|
-
/**
|
|
8223
|
-
* Anchor relative plugin entries to the file that declared them.
|
|
8224
|
-
*
|
|
8225
|
-
* A reference is otherwise resolved against the host startup directory, which is right for
|
|
8226
|
-
* a project file sitting in it and meaningless for `~/.codework/settings.json`, where
|
|
8227
|
-
* `./plugins/x.ts` would name a different file in every project the process is started in.
|
|
8228
|
-
* `file:` URLs, package specs and package names are left exactly as written, and a `plugin`
|
|
8229
|
-
* key is an ID rather than a location. A configuration entry's `package` is anchored like a
|
|
8230
|
-
* module entry, so a relative path names the same module in both spellings.
|
|
8231
|
-
*/
|
|
8232
|
-
const anchored = (entry, file) => {
|
|
8233
|
-
const directory = hostPath.dirname(file);
|
|
8234
|
-
const resolve = (reference) => reference.startsWith("./") || reference.startsWith("../") ? hostPath.resolve(directory, reference) : reference;
|
|
8235
|
-
if (typeof entry === "string") return resolve(entry);
|
|
8236
|
-
return "package" in entry ? {
|
|
8237
|
-
...entry,
|
|
8238
|
-
package: resolve(entry.package)
|
|
8239
|
-
} : entry;
|
|
8240
|
-
};
|
|
8241
|
-
const anchor = (patch, file) => patch.plugins === void 0 ? patch : {
|
|
8242
|
-
...patch,
|
|
8243
|
-
plugins: patch.plugins.map((entry) => anchored(entry, file))
|
|
8244
|
-
};
|
|
8245
|
-
const attempt = (path) => fileSystem.readFileString(path).pipe(Effect.mapError((error) => new SettingsError({
|
|
8246
|
-
path,
|
|
8247
|
-
reason: "read",
|
|
8248
|
-
detail: error.reason._tag
|
|
8249
|
-
})), Effect.flatMap((source) => parse$1(path, source)));
|
|
8250
|
-
/**
|
|
8251
|
-
* One read of every layer. Exported for the harness constructor, which needs the plugin
|
|
8252
|
-
* selection before any layer is built; everything else goes through `Service`.
|
|
8253
|
-
*/
|
|
8254
|
-
const load$1 = Effect.fn("Settings.load")(function* (options) {
|
|
8255
|
-
const root = options.hostDir === void 0 ? void 0 : yield* projectRoot(options.hostDir, options.home);
|
|
8256
|
-
const files = paths({
|
|
8257
|
-
home: options.home,
|
|
8258
|
-
root,
|
|
8259
|
-
...options.userConfigDir === void 0 ? {} : { userConfigDir: options.userConfigDir }
|
|
8260
|
-
});
|
|
8261
|
-
let settings = merge(schema_exports$3.defaults);
|
|
8262
|
-
let declared = schema_exports$3.defaults.declared;
|
|
8263
|
-
for (const group of files) for (const path of group) {
|
|
8264
|
-
const result = yield* Effect.result(attempt(path));
|
|
8265
|
-
if (Result.isSuccess(result)) {
|
|
8266
|
-
const patch = anchor(result.success, path);
|
|
8267
|
-
settings = merge(settings, patch);
|
|
8268
|
-
if (result.success.plugins !== void 0) declared = [...declared, ...result.success.plugins.map((written) => ({
|
|
8269
|
-
written,
|
|
8270
|
-
entry: anchored(written, path),
|
|
8271
|
-
file: path
|
|
8272
|
-
}))];
|
|
8273
|
-
break;
|
|
8274
|
-
}
|
|
8275
|
-
const error = result.failure;
|
|
8276
|
-
if (error.reason === "read" && error.detail === "NotFound") continue;
|
|
8277
|
-
return yield* error;
|
|
8278
|
-
}
|
|
8279
|
-
return {
|
|
8280
|
-
...settings,
|
|
8281
|
-
plugins: declared.map((one) => one.entry),
|
|
8282
|
-
declared
|
|
8283
|
-
};
|
|
8284
|
-
});
|
|
8285
|
-
/**
|
|
8286
|
-
* Each module reference once, owned by the **last** file to declare it (D6).
|
|
8287
|
-
*
|
|
8288
|
-
* The owner is not bookkeeping: its `.npmrc` chain keys the store artifact. The CLI that installs
|
|
8289
|
-
* a reference and the runtime that loads it must pick the same owner, or `plugin update` refreshes
|
|
8290
|
-
* an artifact the runtime never reads. Only a string entry loads a module, so only a string entry
|
|
8291
|
-
* owns one -- a `{ package }` patch in another file configures it and must not move its registry.
|
|
8292
|
-
* A reference keeps the position of its first declaration.
|
|
8293
|
-
*/
|
|
8294
|
-
const modules = (declared) => {
|
|
8295
|
-
const owners = /* @__PURE__ */ new Map();
|
|
8296
|
-
for (const one of declared) if (typeof one.entry === "string") owners.set(one.entry, one);
|
|
8297
|
-
return owners;
|
|
8298
|
-
};
|
|
8299
|
-
const layer$5 = (options = {}) => Layer.effect(Service$2, Effect.gen(function* () {
|
|
8300
|
-
const global = yield* Service$17;
|
|
8301
|
-
return Service$2.of({ load: (hostDir) => load$1({
|
|
8302
|
-
...options,
|
|
8303
|
-
home: global.home,
|
|
8304
|
-
hostDir
|
|
8305
|
-
}) });
|
|
8306
|
-
}));
|
|
8307
|
-
//#endregion
|
|
8308
10450
|
//#region src/state/state.ts
|
|
8309
10451
|
var state_exports = /* @__PURE__ */ __exportAll({
|
|
8310
10452
|
Service: () => Service$1,
|
|
@@ -8354,12 +10496,13 @@ const resolver = (input) => typeof input === "string" ? () => input : input;
|
|
|
8354
10496
|
*/
|
|
8355
10497
|
const layer$4 = (options, pool, references, follow, rebuild, viewOf = () => Effect.undefined) => {
|
|
8356
10498
|
return Layer.effect(Service$1, Effect.gen(function* () {
|
|
8357
|
-
const runtime = yield* Service$
|
|
10499
|
+
const runtime = yield* Service$2;
|
|
8358
10500
|
const sessions = yield* Service$14;
|
|
8359
|
-
const settings = yield* Service$
|
|
10501
|
+
const settings = yield* Service$3;
|
|
8360
10502
|
const eventRegistry = yield* Service$4;
|
|
8361
10503
|
const eventService = yield* Service$16;
|
|
8362
10504
|
const events = makeEvents(eventService);
|
|
10505
|
+
const global = yield* Service$17;
|
|
8363
10506
|
const loading = yield* Semaphore.make(1);
|
|
8364
10507
|
const updated = (origin, id, status) => eventService.publish(PluginUpdated, {
|
|
8365
10508
|
status,
|
|
@@ -8409,6 +10552,38 @@ const layer$4 = (options, pool, references, follow, rebuild, viewOf = () => Effe
|
|
|
8409
10552
|
..."reference" in cause && typeof cause.reference === "string" ? { reference: cause.reference } : {},
|
|
8410
10553
|
...sessionId === void 0 ? {} : { sessionId }
|
|
8411
10554
|
});
|
|
10555
|
+
/** Settings and composed configuration for a session's next exchange. */
|
|
10556
|
+
const configure = Effect.fnUntraced(function* (sessionId) {
|
|
10557
|
+
const session = Option.getOrUndefined(yield* sessions.get(sessionId));
|
|
10558
|
+
const hostDir = session === void 0 ? void 0 : Option.getOrUndefined(session.hostDir);
|
|
10559
|
+
const chosen = session === void 0 ? {} : Option.getOrElse(session.config, () => ({}));
|
|
10560
|
+
const sessionOptions = {
|
|
10561
|
+
...chosen.model === void 0 ? {} : {
|
|
10562
|
+
provider: chosen.model.provider,
|
|
10563
|
+
model: chosen.model.id
|
|
10564
|
+
},
|
|
10565
|
+
...chosen.thinkingLevel === void 0 ? {} : { thinkingLevel: chosen.thinkingLevel },
|
|
10566
|
+
...Option.getOrElse(yield* runtime.get(sessionId), () => ({}))
|
|
10567
|
+
};
|
|
10568
|
+
const loadedSettings = yield* settings.load(hostDir);
|
|
10569
|
+
const composed = compose(loadedSettings, options, sessionOptions);
|
|
10570
|
+
const resolvedModel = yield* resolve$1({
|
|
10571
|
+
provider: composed.provider,
|
|
10572
|
+
model: composed.model,
|
|
10573
|
+
settings: composed.block,
|
|
10574
|
+
models: loadedSettings.models
|
|
10575
|
+
});
|
|
10576
|
+
const thinkingLevel = Model.clampThinkingLevel(resolvedModel, composed.thinkingLevel);
|
|
10577
|
+
return {
|
|
10578
|
+
hostDir,
|
|
10579
|
+
loadedSettings,
|
|
10580
|
+
configured: {
|
|
10581
|
+
...composed,
|
|
10582
|
+
resolvedModel,
|
|
10583
|
+
thinkingLevel
|
|
10584
|
+
}
|
|
10585
|
+
};
|
|
10586
|
+
});
|
|
8412
10587
|
return Service$1.of({
|
|
8413
10588
|
reload: Effect.gen(function* () {
|
|
8414
10589
|
const failures = /* @__PURE__ */ new Map();
|
|
@@ -8427,22 +10602,21 @@ const layer$4 = (options, pool, references, follow, rebuild, viewOf = () => Effe
|
|
|
8427
10602
|
failure: first
|
|
8428
10603
|
};
|
|
8429
10604
|
}),
|
|
10605
|
+
configuration: Effect.fn("State.configuration")(function* (sessionId) {
|
|
10606
|
+
const { configured } = yield* configure(sessionId);
|
|
10607
|
+
return {
|
|
10608
|
+
provider: configured.provider,
|
|
10609
|
+
model: configured.model,
|
|
10610
|
+
thinkingLevel: configured.thinkingLevel
|
|
10611
|
+
};
|
|
10612
|
+
}),
|
|
8430
10613
|
snapshot: Effect.fn("State.snapshot")(function* (sessionId) {
|
|
8431
|
-
const
|
|
8432
|
-
const session = yield* sessions.get(sessionId);
|
|
8433
|
-
const hostDir = Option.isNone(session) ? void 0 : Option.getOrUndefined(session.value.hostDir);
|
|
8434
|
-
const loadedSettings = yield* settings.load(hostDir);
|
|
8435
|
-
const configured = compose(loadedSettings, options, sessionOptions);
|
|
10614
|
+
const { hostDir, loadedSettings, configured } = yield* configure(sessionId);
|
|
8436
10615
|
const { promptCustom, promptSystemAppend, provider: _provider, model: _model, thinkingLevel: _thinkingLevel, toolExecution: _toolExecution, ...rest } = configured.runtime;
|
|
8437
|
-
const { provider, model, thinkingLevel, toolExecution } = configured;
|
|
10616
|
+
const { provider, model, resolvedModel, thinkingLevel, toolExecution } = configured;
|
|
8438
10617
|
const request = merge(resolveOptions(configured.block), rest);
|
|
8439
10618
|
const sandbox = yield* io_exports.SandboxIO.Current;
|
|
8440
10619
|
const location = yield* Service$8;
|
|
8441
|
-
const resolvedModel = yield* resolve$1({
|
|
8442
|
-
provider,
|
|
8443
|
-
model,
|
|
8444
|
-
settings: configured.block
|
|
8445
|
-
});
|
|
8446
10620
|
const refs = references(loadedSettings);
|
|
8447
10621
|
const failed = (cause) => new SnapshotError({
|
|
8448
10622
|
sessionId,
|
|
@@ -8460,7 +10634,15 @@ const layer$4 = (options, pool, references, follow, rebuild, viewOf = () => Effe
|
|
|
8460
10634
|
const chosen = yield* select(refs, loaded).pipe(Effect.mapError(failed));
|
|
8461
10635
|
for (const reference of chosen.missing) {
|
|
8462
10636
|
const where = loadedSettings.declared.find((one) => typeof one.entry === "string" ? one.entry === reference : "package" in one.entry && one.entry.package === reference);
|
|
8463
|
-
|
|
10637
|
+
const error = `plugin ${reference} is configured but not loaded — run \`codework plugin install\`${where === void 0 ? "" : ` (declared in ${where.file})`}`;
|
|
10638
|
+
yield* Effect.logWarning(error);
|
|
10639
|
+
yield* eventService.publish(PluginUpdated, {
|
|
10640
|
+
status: "failed",
|
|
10641
|
+
reference,
|
|
10642
|
+
error,
|
|
10643
|
+
...where === void 0 ? {} : { file: where.file },
|
|
10644
|
+
sessionId
|
|
10645
|
+
});
|
|
8464
10646
|
}
|
|
8465
10647
|
return {
|
|
8466
10648
|
sessionId,
|
|
@@ -8469,6 +10651,7 @@ const layer$4 = (options, pool, references, follow, rebuild, viewOf = () => Effe
|
|
|
8469
10651
|
...yield* run(chosen.selection, {
|
|
8470
10652
|
sessionId,
|
|
8471
10653
|
sandbox,
|
|
10654
|
+
paths: global,
|
|
8472
10655
|
location,
|
|
8473
10656
|
settings: loadedSettings,
|
|
8474
10657
|
model: resolvedModel,
|
|
@@ -8488,13 +10671,34 @@ const layer$4 = (options, pool, references, follow, rebuild, viewOf = () => Effe
|
|
|
8488
10671
|
thinkingLevel,
|
|
8489
10672
|
request,
|
|
8490
10673
|
settings: configured.block,
|
|
8491
|
-
toolExecution
|
|
10674
|
+
toolExecution,
|
|
10675
|
+
retry: loadedSettings.retry
|
|
8492
10676
|
};
|
|
8493
10677
|
})
|
|
8494
10678
|
});
|
|
8495
10679
|
}));
|
|
8496
10680
|
};
|
|
8497
10681
|
//#endregion
|
|
10682
|
+
//#region src/runner/retry.ts
|
|
10683
|
+
/**
|
|
10684
|
+
* Milliseconds to wait before retry `attempt` (1-based), or `undefined` to give up.
|
|
10685
|
+
*
|
|
10686
|
+
* Only a provider failure the provider itself calls transient is retried. A quota error is
|
|
10687
|
+
* excluded even though it often arrives as a retryable 429: waiting does not refill a quota.
|
|
10688
|
+
* Context overflow is a non-retryable 400 and is left to compaction.
|
|
10689
|
+
*
|
|
10690
|
+
* A server-sent `retry-after` raises the delay, never lowers it. One longer than `maxDelayMs`
|
|
10691
|
+
* gives up rather than retrying early into the same rejection.
|
|
10692
|
+
*/
|
|
10693
|
+
const delay = (policy, attempt, error) => {
|
|
10694
|
+
if (attempt > policy.maxRetries) return void 0;
|
|
10695
|
+
if (!Schema.is(ProviderError)(error) || !error.isRetryable) return void 0;
|
|
10696
|
+
if (error.reason._tag === "Runner.ProviderQuotaError") return void 0;
|
|
10697
|
+
const backoff = Math.min(policy.baseDelayMs * 2 ** (attempt - 1), policy.maxDelayMs);
|
|
10698
|
+
const requested = error.reason.retryAfter === void 0 ? 0 : Duration.toMillis(error.reason.retryAfter);
|
|
10699
|
+
return requested > policy.maxDelayMs ? void 0 : Math.max(backoff, Math.ceil(requested));
|
|
10700
|
+
};
|
|
10701
|
+
//#endregion
|
|
8498
10702
|
//#region src/runner/loop.ts
|
|
8499
10703
|
const terminalResult = (text) => ({
|
|
8500
10704
|
content: [{
|
|
@@ -8614,7 +10818,9 @@ const layer$3 = (options = {}) => Layer.effect(Service$7, Effect.gen(function* (
|
|
|
8614
10818
|
sessionId: snapshot.sessionId,
|
|
8615
10819
|
messageId,
|
|
8616
10820
|
callID: call.callID,
|
|
8617
|
-
name: call.name
|
|
10821
|
+
name: call.name,
|
|
10822
|
+
label: snapshot.tools.defs.find((def) => def.name === call.name)?.label,
|
|
10823
|
+
arguments: call.arguments
|
|
8618
10824
|
});
|
|
8619
10825
|
const handled = yield* snapshot.tools.handle(call, {
|
|
8620
10826
|
sessionId: snapshot.sessionId,
|
|
@@ -8661,11 +10867,14 @@ const layer$3 = (options = {}) => Layer.effect(Service$7, Effect.gen(function* (
|
|
|
8661
10867
|
if (leaf !== void 0 && (leaf.stopReason === "stop" || leaf.stopReason === "length")) return { needsContinuation: false };
|
|
8662
10868
|
let publisher;
|
|
8663
10869
|
let committed = false;
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
10870
|
+
let retries = 0;
|
|
10871
|
+
/** Abort the latest attempt's draft, unless its own terminal event already did. */
|
|
10872
|
+
const settleDraft = (cause) => Effect.gen(function* () {
|
|
10873
|
+
if (publisher?.startedMessageId === void 0) return;
|
|
10874
|
+
const stored = yield* sessions.entry(publisher.startedMessageId);
|
|
10875
|
+
if (Option.isSome(stored) && stored.value.entry.state === "draft") yield* publishFailure(stored.value, cause);
|
|
10876
|
+
});
|
|
10877
|
+
const attempt = Effect.gen(function* () {
|
|
8669
10878
|
publisher = yield* make$6({ sessionId }).pipe(Effect.provideService(Service$16, events));
|
|
8670
10879
|
const terminal = yield* requestLLM({
|
|
8671
10880
|
sessionId,
|
|
@@ -8689,6 +10898,43 @@ const layer$3 = (options = {}) => Layer.effect(Service$7, Effect.gen(function* (
|
|
|
8689
10898
|
model: snapshot.model
|
|
8690
10899
|
}, messageFailure(terminal.message));
|
|
8691
10900
|
}
|
|
10901
|
+
return terminal;
|
|
10902
|
+
});
|
|
10903
|
+
const request = () => attempt.pipe(Effect.catch((error) => {
|
|
10904
|
+
const delayMs = delay(snapshot.retry, retries + 1, error);
|
|
10905
|
+
if (delayMs === void 0) return Effect.fail(error);
|
|
10906
|
+
return Effect.gen(function* () {
|
|
10907
|
+
retries += 1;
|
|
10908
|
+
yield* Effect.uninterruptible(settleDraft({
|
|
10909
|
+
_tag: "error",
|
|
10910
|
+
message: error.message
|
|
10911
|
+
}));
|
|
10912
|
+
yield* events.publish(RetryScheduled, {
|
|
10913
|
+
timestamp: yield* DateTime.now,
|
|
10914
|
+
sessionId,
|
|
10915
|
+
attempt: retries,
|
|
10916
|
+
maxRetries: snapshot.retry.maxRetries,
|
|
10917
|
+
delayMs,
|
|
10918
|
+
message: error.message
|
|
10919
|
+
});
|
|
10920
|
+
yield* Effect.sleep(Duration.millis(delayMs));
|
|
10921
|
+
return yield* request();
|
|
10922
|
+
});
|
|
10923
|
+
}));
|
|
10924
|
+
return yield* Effect.gen(function* () {
|
|
10925
|
+
yield* events.publish(TurnStarted, {
|
|
10926
|
+
timestamp: yield* DateTime.now,
|
|
10927
|
+
sessionId
|
|
10928
|
+
});
|
|
10929
|
+
const terminal = yield* request().pipe(Effect.onExit((exit) => retries === 0 ? Effect.void : Effect.gen(function* () {
|
|
10930
|
+
yield* events.publish(RetryFinished, {
|
|
10931
|
+
timestamp: yield* DateTime.now,
|
|
10932
|
+
sessionId,
|
|
10933
|
+
attempt: retries,
|
|
10934
|
+
success: Exit.isSuccess(exit),
|
|
10935
|
+
...Exit.isSuccess(exit) ? {} : { message: Cause.hasInterrupts(exit.cause) ? "interrupted" : errorMessage(exit.cause) }
|
|
10936
|
+
});
|
|
10937
|
+
})));
|
|
8692
10938
|
const interrupted = yield* settleTools(snapshot, terminal.message, terminal.reason, () => {
|
|
8693
10939
|
committed = true;
|
|
8694
10940
|
});
|
|
@@ -8697,24 +10943,20 @@ const layer$3 = (options = {}) => Layer.effect(Service$7, Effect.gen(function* (
|
|
|
8697
10943
|
needsContinuation: terminal.reason === "toolUse",
|
|
8698
10944
|
messageId: terminal.message.messageId
|
|
8699
10945
|
};
|
|
8700
|
-
}).pipe(Effect.
|
|
8701
|
-
if (committed) return Effect.
|
|
10946
|
+
}).pipe(Effect.onError((cause) => {
|
|
10947
|
+
if (committed) return Effect.void;
|
|
8702
10948
|
const turnCause = Cause.hasInterrupts(cause) ? { _tag: "interrupted" } : {
|
|
8703
10949
|
_tag: "error",
|
|
8704
10950
|
message: errorMessage(cause)
|
|
8705
10951
|
};
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
const stored = yield* sessions.entry(publisher.startedMessageId);
|
|
8709
|
-
if (Option.isSome(stored) && stored.value.entry.state === "draft") yield* publishFailure(stored.value, turnCause);
|
|
8710
|
-
}
|
|
10952
|
+
return Effect.gen(function* () {
|
|
10953
|
+
yield* settleDraft(turnCause);
|
|
8711
10954
|
yield* events.publish(TurnAborted, {
|
|
8712
10955
|
timestamp: yield* DateTime.now,
|
|
8713
10956
|
sessionId,
|
|
8714
10957
|
cause: turnCause
|
|
8715
10958
|
});
|
|
8716
10959
|
});
|
|
8717
|
-
return Effect.uninterruptible(record).pipe(Effect.andThen(Effect.failCause(cause)));
|
|
8718
10960
|
}));
|
|
8719
10961
|
});
|
|
8720
10962
|
const runTurn = Effect.fn("Loop.runTurn")(function* (promotion, snapshot) {
|
|
@@ -8868,10 +11110,7 @@ const layer$2 = Layer.effectDiscard(Effect.gen(function* () {
|
|
|
8868
11110
|
messageId: event.data.messageId,
|
|
8869
11111
|
role: "user",
|
|
8870
11112
|
time: { created: DateTime.toEpochMillis(event.data.timestamp) },
|
|
8871
|
-
parts: [
|
|
8872
|
-
type: "text",
|
|
8873
|
-
text: event.data.prompt.text
|
|
8874
|
-
}]
|
|
11113
|
+
parts: [...event.data.prompt.parts]
|
|
8875
11114
|
})).pipe(Effect.orDie);
|
|
8876
11115
|
yield* sessions.append({
|
|
8877
11116
|
id: event.data.messageId,
|
|
@@ -8881,7 +11120,13 @@ const layer$2 = Layer.effectDiscard(Effect.gen(function* () {
|
|
|
8881
11120
|
...encoded,
|
|
8882
11121
|
...event.metadata === void 0 ? {} : { metadata: event.metadata }
|
|
8883
11122
|
}).pipe(Effect.orDie);
|
|
11123
|
+
const title = titleFrom(Prompt.text(event.data.prompt));
|
|
11124
|
+
if (title !== void 0) yield* sessions.retitle(event.data.sessionId, title);
|
|
8884
11125
|
}));
|
|
11126
|
+
yield* events.project(ConfigChanged, (event) => {
|
|
11127
|
+
const { sessionId, timestamp: _timestamp, ...config } = event.data;
|
|
11128
|
+
return sessions.configure(sessionId, config);
|
|
11129
|
+
});
|
|
8885
11130
|
yield* events.project(LLMStarted, (event) => Effect.gen(function* () {
|
|
8886
11131
|
if (event.durable === void 0) return yield* Effect.die("LLMStarted is missing its aggregate sequence");
|
|
8887
11132
|
const encoded = yield* encodeAssistant({
|
|
@@ -8949,10 +11194,10 @@ var harness_exports = /* @__PURE__ */ __exportAll({
|
|
|
8949
11194
|
});
|
|
8950
11195
|
const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
|
|
8951
11196
|
const hostCwd = options.hostCwd ?? process.cwd();
|
|
8952
|
-
const paths = yield* resolve$
|
|
8953
|
-
const global = layerWith
|
|
11197
|
+
const paths = yield* resolve$4(options.home === void 0 ? {} : { home: hostPath.resolve(hostCwd, expandTilde(options.home, hostPath)) });
|
|
11198
|
+
const global = layerWith(paths);
|
|
8954
11199
|
const settingsOptions = options.userConfigDir === void 0 ? {} : { userConfigDir: userConfigDir(options.userConfigDir, hostCwd) };
|
|
8955
|
-
const config = yield* load$
|
|
11200
|
+
const config = yield* load$3({
|
|
8956
11201
|
...settingsOptions,
|
|
8957
11202
|
home: paths.home
|
|
8958
11203
|
});
|
|
@@ -8969,12 +11214,12 @@ const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
|
|
|
8969
11214
|
* `hostDir` led to; only the process view falls back to `hostCwd`.
|
|
8970
11215
|
*/
|
|
8971
11216
|
const hostDirOf = (root) => root ?? hostCwd;
|
|
8972
|
-
const filed = (reference, from) => parse$
|
|
11217
|
+
const filed = (reference, from) => parse$1(reference, from).pipe(Effect.flatMap((target) => target.kind === "local" ? fileSystem.exists(target.path).pipe(Effect.map((there) => there ? Option.some({}) : Option.none())) : resolve$2(target, paths.cache, from).pipe(Effect.map((entry) => Option.fromUndefinedOr(entry)))), Effect.orElseSucceed(() => Option.none()));
|
|
8973
11218
|
const resolveOnly = {
|
|
8974
11219
|
...catalogOptions,
|
|
8975
11220
|
install: required
|
|
8976
11221
|
};
|
|
8977
|
-
const pool = yield* Ref.make(yield* load$
|
|
11222
|
+
const pool = yield* Ref.make(yield* load$1(references(config), {
|
|
8978
11223
|
...resolveOnly,
|
|
8979
11224
|
declared: declaredIn(config)
|
|
8980
11225
|
}));
|
|
@@ -8988,7 +11233,7 @@ const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
|
|
|
8988
11233
|
let reloads = 0;
|
|
8989
11234
|
const rebuild = (root, loaded) => Effect.gen(function* () {
|
|
8990
11235
|
reloads += 1;
|
|
8991
|
-
const current = yield* load$
|
|
11236
|
+
const current = yield* load$3({
|
|
8992
11237
|
...settingsOptions,
|
|
8993
11238
|
home: paths.home,
|
|
8994
11239
|
...root === void 0 ? {} : { hostDir: root }
|
|
@@ -8996,7 +11241,7 @@ const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
|
|
|
8996
11241
|
const retained = [];
|
|
8997
11242
|
const configured = references(current);
|
|
8998
11243
|
for (const origin of loaded.origins.values()) {
|
|
8999
|
-
if (configured.includes(origin.reference) || Option.isSome(yield* filed(origin.reference, anchor
|
|
11244
|
+
if (configured.includes(origin.reference) || Option.isSome(yield* filed(origin.reference, anchor(origin.file, hostDirOf(root))))) {
|
|
9000
11245
|
retained.push(origin);
|
|
9001
11246
|
continue;
|
|
9002
11247
|
}
|
|
@@ -9006,7 +11251,7 @@ const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
|
|
|
9006
11251
|
const known = /* @__PURE__ */ new Map();
|
|
9007
11252
|
for (const origin of retained) if (origin.file !== void 0) known.set(origin.reference, origin.file);
|
|
9008
11253
|
for (const [reference, file] of declaredIn(current)) known.set(reference, file);
|
|
9009
|
-
return yield* load$
|
|
11254
|
+
return yield* load$1(accumulated, {
|
|
9010
11255
|
...resolveOnly,
|
|
9011
11256
|
hostDir: hostDirOf(root),
|
|
9012
11257
|
declared: known,
|
|
@@ -9021,10 +11266,10 @@ const layer = (options = {}) => Layer.unwrap(Effect.gen(function* () {
|
|
|
9021
11266
|
const authFile$1 = authFile(options.home === void 0 ? void 0 : paths.home);
|
|
9022
11267
|
const open = options.llm ?? openWith(authFile$1 === void 0 ? {} : { authFile: authFile$1 });
|
|
9023
11268
|
const loop = layer$3({ request: make$5(open) });
|
|
9024
|
-
return layer$10.pipe(Layer.provideMerge(layer$
|
|
11269
|
+
return layer$10.pipe(Layer.provideMerge(layer$6.pipe(Layer.provide(loop))), Layer.provideMerge(layer$4({}, pool, references, followStore, rebuild, viewOf)), Layer.provideMerge(layer$8(settingsOptions)), Layer.provideMerge(layer$5), Layer.provideMerge(sandboxes), Layer.provideMerge(layer$22), Layer.provideMerge(layer$1), Layer.provideMerge(layer$25), Layer.provideMerge(layer$9(definitions)), Layer.provideMerge(database), Layer.provideMerge(global), Layer.provideMerge(layer$7({
|
|
9025
11270
|
home: paths.home,
|
|
9026
11271
|
watch: options.watchModels ?? false
|
|
9027
|
-
})));
|
|
11272
|
+
})), Layer.provideMerge(Layer.succeed(Credentials, { authFile: authFile$1 })));
|
|
9028
11273
|
}));
|
|
9029
11274
|
//#endregion
|
|
9030
11275
|
//#region src/effect/sandbox.ts
|
|
@@ -9151,30 +11396,54 @@ var session_exports = /* @__PURE__ */ __exportAll({
|
|
|
9151
11396
|
SessionMessageSchema: () => schema_exports$4,
|
|
9152
11397
|
SessionSchema: () => schema_exports$8,
|
|
9153
11398
|
attach: () => attach,
|
|
11399
|
+
configuration: () => configuration,
|
|
9154
11400
|
create: () => create$1,
|
|
9155
11401
|
get: () => get,
|
|
9156
11402
|
link: () => link,
|
|
9157
11403
|
relink: () => relink
|
|
9158
11404
|
});
|
|
11405
|
+
/** What stays in this process. The model and thinking level are the session's {@link chosen} config. */
|
|
9159
11406
|
const runtimeBindings = (input) => ({
|
|
9160
11407
|
...input.model?.options,
|
|
9161
|
-
...input.model === void 0 ? {} : {
|
|
9162
|
-
provider: input.model.provider,
|
|
9163
|
-
model: input.model.id
|
|
9164
|
-
},
|
|
9165
|
-
...input.thinkingLevel === void 0 ? {} : { thinkingLevel: input.thinkingLevel },
|
|
9166
11408
|
...input.tools?.execution === void 0 ? {} : { toolExecution: input.tools.execution },
|
|
9167
11409
|
...input.systemPrompt?.custom === void 0 ? {} : { promptCustom: input.systemPrompt.custom },
|
|
9168
11410
|
...input.systemPrompt?.append === void 0 ? {} : { promptSystemAppend: input.systemPrompt.append }
|
|
9169
11411
|
});
|
|
9170
|
-
|
|
11412
|
+
/** The keys of the session's durable config this input names. */
|
|
11413
|
+
const chosen = (input) => ({
|
|
11414
|
+
...input.model === void 0 ? {} : { model: {
|
|
11415
|
+
provider: input.model.provider,
|
|
11416
|
+
id: input.model.id
|
|
11417
|
+
} },
|
|
11418
|
+
...input.thinkingLevel === void 0 ? {} : { thinkingLevel: input.thinkingLevel }
|
|
11419
|
+
});
|
|
11420
|
+
/** Records a config choice; the projector merges it into the session row in the same commit. */
|
|
11421
|
+
const choose = Effect.fn("Session.choose")(function* (sessionId, input) {
|
|
11422
|
+
const config = chosen(input);
|
|
11423
|
+
if (Object.keys(config).length === 0) return;
|
|
11424
|
+
yield* (yield* Service$16).publish(ConfigChanged, {
|
|
11425
|
+
sessionId,
|
|
11426
|
+
timestamp: yield* DateTime.now,
|
|
11427
|
+
...config
|
|
11428
|
+
});
|
|
11429
|
+
});
|
|
11430
|
+
const promptInput = Effect.fnUntraced(function* (input) {
|
|
9171
11431
|
const value = typeof input === "string" ? { text: input } : input;
|
|
11432
|
+
const parts = "parts" in value ? value.parts : [{
|
|
11433
|
+
type: "text",
|
|
11434
|
+
text: value.text
|
|
11435
|
+
}];
|
|
11436
|
+
if (parts.length === 0) return yield* new InvalidPromptError({ reason: "A prompt needs at least one part" });
|
|
11437
|
+
for (const part of parts) {
|
|
11438
|
+
const problem = part.type === "image" ? imageProblem(part) : void 0;
|
|
11439
|
+
if (problem !== void 0) return yield* new InvalidPromptError({ reason: problem });
|
|
11440
|
+
}
|
|
9172
11441
|
return {
|
|
9173
|
-
prompt: Prompt.make({
|
|
11442
|
+
prompt: Prompt.make({ parts }),
|
|
9174
11443
|
...value.delivery === void 0 ? {} : { delivery: value.delivery },
|
|
9175
11444
|
...value.id === void 0 ? {} : { id: value.id }
|
|
9176
11445
|
};
|
|
9177
|
-
};
|
|
11446
|
+
});
|
|
9178
11447
|
const makeHandle = Effect.fn("Session.makeHandle")(function* (id) {
|
|
9179
11448
|
const sessions = yield* Service$14;
|
|
9180
11449
|
const control = yield* Service$5;
|
|
@@ -9200,14 +11469,14 @@ const makeHandle = Effect.fn("Session.makeHandle")(function* (id) {
|
|
|
9200
11469
|
id,
|
|
9201
11470
|
active: control.active.pipe(Effect.map((active) => active.has(id))),
|
|
9202
11471
|
info,
|
|
9203
|
-
prompt: (input) => control.prompt({
|
|
11472
|
+
prompt: (input) => promptInput(input).pipe(Effect.flatMap((prompt) => control.prompt({
|
|
9204
11473
|
sessionId: id,
|
|
9205
|
-
...
|
|
9206
|
-
}),
|
|
9207
|
-
run: (input) => control.run({
|
|
11474
|
+
...prompt
|
|
11475
|
+
}))),
|
|
11476
|
+
run: (input) => promptInput(input).pipe(Effect.flatMap((prompt) => control.run({
|
|
9208
11477
|
sessionId: id,
|
|
9209
|
-
...
|
|
9210
|
-
}),
|
|
11478
|
+
...prompt
|
|
11479
|
+
}))),
|
|
9211
11480
|
wait: () => control.wait(id),
|
|
9212
11481
|
resume: () => control.resume(id),
|
|
9213
11482
|
interrupt: () => control.interrupt(id, { awaitSettlement: true }),
|
|
@@ -9217,7 +11486,7 @@ const makeHandle = Effect.fn("Session.makeHandle")(function* (id) {
|
|
|
9217
11486
|
});
|
|
9218
11487
|
const create$1 = Effect.fn("Session.create")(function* (input = {}) {
|
|
9219
11488
|
const sessions = yield* Service$14;
|
|
9220
|
-
const runtime = yield* Service$
|
|
11489
|
+
const runtime = yield* Service$2;
|
|
9221
11490
|
const sandboxes = yield* Controller;
|
|
9222
11491
|
const id = ID$1.create();
|
|
9223
11492
|
const sandboxId = input.sandbox?.id ?? instance_exports.SandboxInstance.ID.local;
|
|
@@ -9226,11 +11495,11 @@ const create$1 = Effect.fn("Session.create")(function* (input = {}) {
|
|
|
9226
11495
|
id,
|
|
9227
11496
|
spaceId: location.space.id,
|
|
9228
11497
|
directory: location.directory,
|
|
9229
|
-
slug: id,
|
|
9230
11498
|
title: input.title ?? "Session",
|
|
9231
11499
|
...input.hostDir === void 0 ? {} : { hostDir: declaredHostDir(input.hostDir) }
|
|
9232
11500
|
});
|
|
9233
11501
|
yield* runtime.set(id, runtimeBindings(input));
|
|
11502
|
+
yield* choose(id, input);
|
|
9234
11503
|
return yield* makeHandle(id);
|
|
9235
11504
|
});
|
|
9236
11505
|
/**
|
|
@@ -9296,9 +11565,20 @@ const get = Effect.fn("Session.get")(function* (sessionId) {
|
|
|
9296
11565
|
const attach = Effect.fn("Session.attach")(function* (input) {
|
|
9297
11566
|
const found = yield* get(input.sessionId);
|
|
9298
11567
|
if (Option.isNone(found)) return yield* new SessionNotFoundError({ sessionId: input.sessionId });
|
|
9299
|
-
yield* (yield* Service$
|
|
11568
|
+
yield* (yield* Service$2).update(input.sessionId, runtimeBindings(input));
|
|
11569
|
+
yield* choose(input.sessionId, input);
|
|
9300
11570
|
return found.value;
|
|
9301
11571
|
});
|
|
11572
|
+
/**
|
|
11573
|
+
* The model and thinking level the session's next exchange runs with. A choice made through
|
|
11574
|
+
* {@link create} or {@link attach} is saved with the session and outlives the process; without
|
|
11575
|
+
* one, the session follows its settings.
|
|
11576
|
+
*/
|
|
11577
|
+
const configuration = Effect.fn("Session.configuration")(function* (sessionId) {
|
|
11578
|
+
const sessions = yield* Service$14;
|
|
11579
|
+
if (Option.isNone(yield* sessions.get(sessionId))) return yield* new SessionNotFoundError({ sessionId });
|
|
11580
|
+
return yield* Service$1.use((state) => state.configuration(sessionId));
|
|
11581
|
+
});
|
|
9302
11582
|
//#endregion
|
|
9303
11583
|
//#region src/plugin/prompt/schema.ts
|
|
9304
11584
|
var schema_exports$1 = /* @__PURE__ */ __exportAll({});
|
|
@@ -9310,11 +11590,12 @@ var plugin_exports = /* @__PURE__ */ __exportAll({
|
|
|
9310
11590
|
InstallError: () => InstallError,
|
|
9311
11591
|
LoadError: () => LoadError,
|
|
9312
11592
|
Prompt: () => schema_exports$1,
|
|
11593
|
+
Section: () => Section,
|
|
9313
11594
|
SetupError: () => SetupError,
|
|
9314
11595
|
SourceError: () => SourceError,
|
|
9315
11596
|
StoreError: () => StoreError,
|
|
9316
11597
|
Tool: () => schema_exports$2,
|
|
9317
|
-
anchor: () => anchor
|
|
11598
|
+
anchor: () => anchor,
|
|
9318
11599
|
canonical: () => canonical,
|
|
9319
11600
|
check: () => check,
|
|
9320
11601
|
define: () => plugin_exports$1.define,
|
|
@@ -9322,7 +11603,7 @@ var plugin_exports = /* @__PURE__ */ __exportAll({
|
|
|
9322
11603
|
identity: () => digest,
|
|
9323
11604
|
inspect: () => inspect,
|
|
9324
11605
|
localName: () => localName,
|
|
9325
|
-
parse: () => parse$
|
|
11606
|
+
parse: () => parse$1,
|
|
9326
11607
|
probe: () => probe,
|
|
9327
11608
|
resolve: () => resolve$2,
|
|
9328
11609
|
resolveCached: () => required,
|