@xandreed/core 0.3.0
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/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/domain/agent-failure.entity.d.ts +12 -0
- package/dist/domain/agent-failure.entity.functions.d.ts +5 -0
- package/dist/domain/agent-failure.entity.functions.js +8 -0
- package/dist/domain/agent-failure.entity.js +8 -0
- package/dist/domain/failure.entity.d.ts +12 -0
- package/dist/domain/failure.entity.functions.d.ts +3 -0
- package/dist/domain/failure.entity.functions.js +1 -0
- package/dist/domain/failure.entity.js +5 -0
- package/dist/domain/loop-event.entity.d.ts +94 -0
- package/dist/domain/loop-event.entity.js +0 -0
- package/dist/domain/message.entity.d.ts +227 -0
- package/dist/domain/message.entity.js +51 -0
- package/dist/domain/model-call-policy.entity.d.ts +12 -0
- package/dist/domain/model-call-policy.entity.js +5 -0
- package/dist/domain/model-catalog.entity.d.ts +10 -0
- package/dist/domain/model-catalog.entity.js +7 -0
- package/dist/domain/model-selection.entity.d.ts +24 -0
- package/dist/domain/model-selection.entity.functions.d.ts +5 -0
- package/dist/domain/model-selection.entity.functions.js +9 -0
- package/dist/domain/model-selection.entity.js +8 -0
- package/dist/domain/prompt-provenance.entity.d.ts +11 -0
- package/dist/domain/prompt-provenance.entity.js +8 -0
- package/dist/domain/token-usage.entity.d.ts +9 -0
- package/dist/domain/token-usage.entity.functions.d.ts +3 -0
- package/dist/domain/token-usage.entity.functions.js +11 -0
- package/dist/domain/token-usage.entity.js +7 -0
- package/dist/harness/capability.entity.d.ts +61 -0
- package/dist/harness/capability.entity.functions.d.ts +6 -0
- package/dist/harness/capability.entity.functions.js +10 -0
- package/dist/harness/capability.entity.js +26 -0
- package/dist/harness/capability.test.d.ts +1 -0
- package/dist/harness/config.entity.d.ts +48 -0
- package/dist/harness/config.entity.functions.d.ts +3 -0
- package/dist/harness/config.entity.functions.js +1 -0
- package/dist/harness/config.entity.js +18 -0
- package/dist/harness/plugin.adapter.d.ts +18 -0
- package/dist/harness/plugin.adapter.js +13 -0
- package/dist/harness/plugin.entity.d.ts +25 -0
- package/dist/harness/plugin.entity.js +9 -0
- package/dist/harness/session.entity.d.ts +33 -0
- package/dist/harness/session.entity.js +25 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +99 -0
- package/dist/loop/cacheKey.d.ts +13 -0
- package/dist/loop/cacheKey.js +2 -0
- package/dist/loop/mapping.d.ts +79 -0
- package/dist/loop/mapping.js +154 -0
- package/dist/loop/mapping.test.d.ts +1 -0
- package/dist/loop/modelPolicy.d.ts +21 -0
- package/dist/loop/modelPolicy.js +2 -0
- package/dist/loop/promptProvenance.d.ts +8 -0
- package/dist/loop/promptProvenance.js +2 -0
- package/dist/loop/stepContext.d.ts +3 -0
- package/dist/loop/stepContext.js +2 -0
- package/dist/loop/streamFold.d.ts +31 -0
- package/dist/loop/streamFold.js +58 -0
- package/dist/loop/streamFold.test.d.ts +1 -0
- package/dist/mcp/bridge.d.ts +51 -0
- package/dist/mcp/bridge.js +70 -0
- package/dist/mcp/bridge.test.d.ts +1 -0
- package/dist/ports/auth-store.port.d.ts +50 -0
- package/dist/ports/auth-store.port.js +18 -0
- package/dist/ports/capability.port.d.ts +15 -0
- package/dist/ports/capability.port.js +7 -0
- package/dist/ports/conversation-store.port.d.ts +250 -0
- package/dist/ports/conversation-store.port.js +36 -0
- package/dist/ports/file-system.port.d.ts +26 -0
- package/dist/ports/file-system.port.js +10 -0
- package/dist/ports/harness.port.d.ts +106 -0
- package/dist/ports/harness.port.js +31 -0
- package/dist/ports/mcp-client.port.d.ts +74 -0
- package/dist/ports/mcp-client.port.js +24 -0
- package/dist/ports/model-catalog.port.d.ts +10 -0
- package/dist/ports/model-catalog.port.js +4 -0
- package/dist/ports/settings-store.port.d.ts +85 -0
- package/dist/ports/settings-store.port.js +36 -0
- package/dist/ports/shell.port.d.ts +44 -0
- package/dist/ports/shell.port.js +16 -0
- package/dist/ports/utility-llm.port.d.ts +48 -0
- package/dist/ports/utility-llm.port.js +16 -0
- package/dist/session/chassis.d.ts +59 -0
- package/dist/session/chassis.js +27 -0
- package/dist/session/chassis.test.d.ts +1 -0
- package/dist/spec/SpecDoc.d.ts +162 -0
- package/dist/spec/SpecDoc.js +59 -0
- package/dist/spec/codec.d.ts +30 -0
- package/dist/spec/codec.js +123 -0
- package/dist/spec/codec.test.d.ts +1 -0
- package/dist/spec/frontmatter.d.ts +16 -0
- package/dist/spec/frontmatter.js +25 -0
- package/dist/util/json.d.ts +19 -0
- package/dist/util/json.js +15 -0
- package/package.json +30 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Context, Option, Schema } from "effect";
|
|
2
|
+
import type { Effect } from "effect";
|
|
3
|
+
declare const EngineSettings_base: Schema.Class<EngineSettings, {
|
|
4
|
+
model: Schema.PropertySignature<":", Option.Option<string>, never, "?:", string | undefined, true, never>;
|
|
5
|
+
codeModel: Schema.PropertySignature<":", Option.Option<string>, never, "?:", string | undefined, true, never>;
|
|
6
|
+
fastModel: Schema.PropertySignature<":", Option.Option<string>, never, "?:", string | undefined, true, never>;
|
|
7
|
+
fallbackModel: Schema.PropertySignature<":", Option.Option<string>, never, "?:", string | undefined, true, never>;
|
|
8
|
+
sandbox: Schema.PropertySignature<":", Option.Option<boolean>, never, "?:", boolean | undefined, true, never>;
|
|
9
|
+
viMode: Schema.PropertySignature<":", Option.Option<boolean>, never, "?:", boolean | undefined, true, never>;
|
|
10
|
+
maxAttempts: Schema.PropertySignature<":", Option.Option<number>, never, "?:", number | undefined, true, never>;
|
|
11
|
+
budgetMillis: Schema.PropertySignature<":", Option.Option<number>, never, "?:", number | undefined, true, never>;
|
|
12
|
+
/** Selected after the model because the accepted levels are model-specific. */
|
|
13
|
+
reasoningEffort: Schema.PropertySignature<":", Option.Option<"none" | "low" | "medium" | "high" | "xhigh" | "max">, never, "?:", "none" | "low" | "medium" | "high" | "xhigh" | "max" | undefined, true, never>;
|
|
14
|
+
codeReasoningEffort: Schema.PropertySignature<":", Option.Option<"none" | "low" | "medium" | "high" | "xhigh" | "max">, never, "?:", "none" | "low" | "medium" | "high" | "xhigh" | "max" | undefined, true, never>;
|
|
15
|
+
fastReasoningEffort: Schema.PropertySignature<":", Option.Option<"none" | "low" | "medium" | "high" | "xhigh" | "max">, never, "?:", "none" | "low" | "medium" | "high" | "xhigh" | "max" | undefined, true, never>;
|
|
16
|
+
}, Schema.Struct.Encoded<{
|
|
17
|
+
model: Schema.PropertySignature<":", Option.Option<string>, never, "?:", string | undefined, true, never>;
|
|
18
|
+
codeModel: Schema.PropertySignature<":", Option.Option<string>, never, "?:", string | undefined, true, never>;
|
|
19
|
+
fastModel: Schema.PropertySignature<":", Option.Option<string>, never, "?:", string | undefined, true, never>;
|
|
20
|
+
fallbackModel: Schema.PropertySignature<":", Option.Option<string>, never, "?:", string | undefined, true, never>;
|
|
21
|
+
sandbox: Schema.PropertySignature<":", Option.Option<boolean>, never, "?:", boolean | undefined, true, never>;
|
|
22
|
+
viMode: Schema.PropertySignature<":", Option.Option<boolean>, never, "?:", boolean | undefined, true, never>;
|
|
23
|
+
maxAttempts: Schema.PropertySignature<":", Option.Option<number>, never, "?:", number | undefined, true, never>;
|
|
24
|
+
budgetMillis: Schema.PropertySignature<":", Option.Option<number>, never, "?:", number | undefined, true, never>;
|
|
25
|
+
/** Selected after the model because the accepted levels are model-specific. */
|
|
26
|
+
reasoningEffort: Schema.PropertySignature<":", Option.Option<"none" | "low" | "medium" | "high" | "xhigh" | "max">, never, "?:", "none" | "low" | "medium" | "high" | "xhigh" | "max" | undefined, true, never>;
|
|
27
|
+
codeReasoningEffort: Schema.PropertySignature<":", Option.Option<"none" | "low" | "medium" | "high" | "xhigh" | "max">, never, "?:", "none" | "low" | "medium" | "high" | "xhigh" | "max" | undefined, true, never>;
|
|
28
|
+
fastReasoningEffort: Schema.PropertySignature<":", Option.Option<"none" | "low" | "medium" | "high" | "xhigh" | "max">, never, "?:", "none" | "low" | "medium" | "high" | "xhigh" | "max" | undefined, true, never>;
|
|
29
|
+
}>, never, {
|
|
30
|
+
readonly model?: Option.Option<string>;
|
|
31
|
+
} & {
|
|
32
|
+
readonly codeModel?: Option.Option<string>;
|
|
33
|
+
} & {
|
|
34
|
+
readonly fastModel?: Option.Option<string>;
|
|
35
|
+
} & {
|
|
36
|
+
readonly fallbackModel?: Option.Option<string>;
|
|
37
|
+
} & {
|
|
38
|
+
readonly sandbox?: Option.Option<boolean>;
|
|
39
|
+
} & {
|
|
40
|
+
readonly viMode?: Option.Option<boolean>;
|
|
41
|
+
} & {
|
|
42
|
+
readonly maxAttempts?: Option.Option<number>;
|
|
43
|
+
} & {
|
|
44
|
+
readonly budgetMillis?: Option.Option<number>;
|
|
45
|
+
} & {
|
|
46
|
+
readonly reasoningEffort?: Option.Option<"none" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
47
|
+
} & {
|
|
48
|
+
readonly codeReasoningEffort?: Option.Option<"none" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
49
|
+
} & {
|
|
50
|
+
readonly fastReasoningEffort?: Option.Option<"none" | "low" | "medium" | "high" | "xhigh" | "max">;
|
|
51
|
+
}, {}, {}>;
|
|
52
|
+
export declare class EngineSettings extends EngineSettings_base {
|
|
53
|
+
}
|
|
54
|
+
declare const SettingsError_base: Schema.TaggedErrorClass<SettingsError, "SettingsError", {
|
|
55
|
+
readonly _tag: Schema.tag<"SettingsError">;
|
|
56
|
+
} & {
|
|
57
|
+
message: typeof Schema.String;
|
|
58
|
+
}>;
|
|
59
|
+
export declare class SettingsError extends SettingsError_base {
|
|
60
|
+
}
|
|
61
|
+
/** The three model roles a selection can be persisted under. */
|
|
62
|
+
export type ModelRole = "general" | "code" | "fast";
|
|
63
|
+
/** The non-role keys the keyed setter may write — a closed vocabulary, so a
|
|
64
|
+
* typo can never land a dead key in the user's config. */
|
|
65
|
+
export declare const SETTINGS_KEYS: readonly ["fallbackModel", "sandbox", "viMode", "maxAttempts", "budgetMillis", "reasoningEffort", "codeReasoningEffort", "fastReasoningEffort"];
|
|
66
|
+
export type SettingsKey = (typeof SETTINGS_KEYS)[number];
|
|
67
|
+
declare const SettingsStore_base: Context.TagClass<SettingsStore, "@xandreed/core/SettingsStore", {
|
|
68
|
+
readonly load: Effect.Effect<EngineSettings, SettingsError>;
|
|
69
|
+
/** Persist one role's model selection (the human's `:model` action);
|
|
70
|
+
* `None` clears the role so it falls back to its default again. */
|
|
71
|
+
readonly setRole: (role: ModelRole, selection: Option.Option<string>) => Effect.Effect<void, SettingsError>;
|
|
72
|
+
/** Persist one non-role key from its STRING form (the `:settings`
|
|
73
|
+
* action): the adapter validates + coerces per key ("true", "3",
|
|
74
|
+
* "provider:model") and rejects a value that doesn't parse; `None`
|
|
75
|
+
* clears the key. */
|
|
76
|
+
readonly set: (key: SettingsKey, value: Option.Option<string>) => Effect.Effect<void, SettingsError>;
|
|
77
|
+
}>;
|
|
78
|
+
/**
|
|
79
|
+
* Read-through settings port. `load` re-reads the backing store on every call
|
|
80
|
+
* so a value changed mid-session takes effect on the next turn — callers must
|
|
81
|
+
* not cache the result across turns.
|
|
82
|
+
*/
|
|
83
|
+
export declare class SettingsStore extends SettingsStore_base {
|
|
84
|
+
}
|
|
85
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Context, Option, Schema } from "effect";
|
|
2
|
+
import { ReasoningEffort } from "../domain/model-call-policy.entity.js";
|
|
3
|
+
const optionField = (schema) => Schema.optionalWith(schema, { as: "Option" }).pipe(Schema.withConstructorDefault(Option.none));
|
|
4
|
+
|
|
5
|
+
export class EngineSettings extends Schema.Class("EngineSettings")({
|
|
6
|
+
model: optionField(Schema.String),
|
|
7
|
+
codeModel: optionField(Schema.String),
|
|
8
|
+
fastModel: optionField(Schema.String),
|
|
9
|
+
fallbackModel: optionField(Schema.String),
|
|
10
|
+
sandbox: optionField(Schema.Boolean),
|
|
11
|
+
viMode: optionField(Schema.Boolean),
|
|
12
|
+
maxAttempts: optionField(Schema.Number),
|
|
13
|
+
budgetMillis: optionField(Schema.Number),
|
|
14
|
+
reasoningEffort: optionField(ReasoningEffort),
|
|
15
|
+
codeReasoningEffort: optionField(ReasoningEffort),
|
|
16
|
+
fastReasoningEffort: optionField(ReasoningEffort)
|
|
17
|
+
}) {
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class SettingsError extends Schema.TaggedError()("SettingsError", {
|
|
21
|
+
message: Schema.String
|
|
22
|
+
}) {
|
|
23
|
+
}
|
|
24
|
+
export const SETTINGS_KEYS = [
|
|
25
|
+
"fallbackModel",
|
|
26
|
+
"sandbox",
|
|
27
|
+
"viMode",
|
|
28
|
+
"maxAttempts",
|
|
29
|
+
"budgetMillis",
|
|
30
|
+
"reasoningEffort",
|
|
31
|
+
"codeReasoningEffort",
|
|
32
|
+
"fastReasoningEffort"
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export class SettingsStore extends Context.Tag("@xandreed/core/SettingsStore")() {
|
|
36
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Context, Schema } from "effect";
|
|
2
|
+
import type { Effect } from "effect";
|
|
3
|
+
declare const ShellError_base: Schema.TaggedErrorClass<ShellError, "ShellError", {
|
|
4
|
+
readonly _tag: Schema.tag<"ShellError">;
|
|
5
|
+
} & {
|
|
6
|
+
message: typeof Schema.String;
|
|
7
|
+
}>;
|
|
8
|
+
export declare class ShellError extends ShellError_base {
|
|
9
|
+
}
|
|
10
|
+
declare const ShellResult_base: Schema.Class<ShellResult, {
|
|
11
|
+
stdout: typeof Schema.String;
|
|
12
|
+
stderr: typeof Schema.String;
|
|
13
|
+
exitCode: typeof Schema.Number;
|
|
14
|
+
}, Schema.Struct.Encoded<{
|
|
15
|
+
stdout: typeof Schema.String;
|
|
16
|
+
stderr: typeof Schema.String;
|
|
17
|
+
exitCode: typeof Schema.Number;
|
|
18
|
+
}>, never, {
|
|
19
|
+
readonly stdout: string;
|
|
20
|
+
} & {
|
|
21
|
+
readonly stderr: string;
|
|
22
|
+
} & {
|
|
23
|
+
readonly exitCode: number;
|
|
24
|
+
}, {}, {}>;
|
|
25
|
+
export declare class ShellResult extends ShellResult_base {
|
|
26
|
+
}
|
|
27
|
+
declare const Shell_base: Context.TagClass<Shell, "@xandreed/core/Shell", {
|
|
28
|
+
readonly exec: (command: string, options?: {
|
|
29
|
+
readonly cwd?: string;
|
|
30
|
+
readonly timeoutMs?: number;
|
|
31
|
+
/** BEST-EFFORT live tap: called with each output chunk (stdout and
|
|
32
|
+
* stderr) as it arrives, so a long build stops looking dead. Sync
|
|
33
|
+
* and advisory — the settled `ShellResult` stays the contract. */
|
|
34
|
+
readonly onChunk?: (chunk: string) => void;
|
|
35
|
+
}) => Effect.Effect<ShellResult, ShellError>;
|
|
36
|
+
}>;
|
|
37
|
+
/**
|
|
38
|
+
* Subprocess execution. A non-zero exit is a RESULT (the model reads the
|
|
39
|
+
* code + stderr and adapts), never an error — the error channel carries only
|
|
40
|
+
* spawn/timeout infrastructure failures.
|
|
41
|
+
*/
|
|
42
|
+
export declare class Shell extends Shell_base {
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Context, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
export class ShellError extends Schema.TaggedError()("ShellError", {
|
|
4
|
+
message: Schema.String
|
|
5
|
+
}) {
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class ShellResult extends Schema.Class("ShellResult")({
|
|
9
|
+
stdout: Schema.String,
|
|
10
|
+
stderr: Schema.String,
|
|
11
|
+
exitCode: Schema.Number
|
|
12
|
+
}) {
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class Shell extends Context.Tag("@xandreed/core/Shell")() {
|
|
16
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Context, Schema } from "effect";
|
|
2
|
+
import type { Effect } from "effect";
|
|
3
|
+
declare const UtilityError_base: Schema.TaggedErrorClass<UtilityError, "UtilityError", {
|
|
4
|
+
readonly _tag: Schema.tag<"UtilityError">;
|
|
5
|
+
} & {
|
|
6
|
+
message: typeof Schema.String;
|
|
7
|
+
}>;
|
|
8
|
+
export declare class UtilityError extends UtilityError_base {
|
|
9
|
+
}
|
|
10
|
+
declare const UtilityCompletion_base: Schema.Class<UtilityCompletion, {
|
|
11
|
+
text: typeof Schema.String;
|
|
12
|
+
usage: Schema.Struct<{
|
|
13
|
+
inputTokens: typeof Schema.Number;
|
|
14
|
+
outputTokens: typeof Schema.Number;
|
|
15
|
+
totalTokens: typeof Schema.Number;
|
|
16
|
+
cacheReadTokens: typeof Schema.Number;
|
|
17
|
+
}>;
|
|
18
|
+
}, Schema.Struct.Encoded<{
|
|
19
|
+
text: typeof Schema.String;
|
|
20
|
+
usage: Schema.Struct<{
|
|
21
|
+
inputTokens: typeof Schema.Number;
|
|
22
|
+
outputTokens: typeof Schema.Number;
|
|
23
|
+
totalTokens: typeof Schema.Number;
|
|
24
|
+
cacheReadTokens: typeof Schema.Number;
|
|
25
|
+
}>;
|
|
26
|
+
}>, never, {
|
|
27
|
+
readonly text: string;
|
|
28
|
+
} & {
|
|
29
|
+
readonly usage: {
|
|
30
|
+
readonly inputTokens: number;
|
|
31
|
+
readonly outputTokens: number;
|
|
32
|
+
readonly totalTokens: number;
|
|
33
|
+
readonly cacheReadTokens: number;
|
|
34
|
+
};
|
|
35
|
+
}, {}, {}>;
|
|
36
|
+
export declare class UtilityCompletion extends UtilityCompletion_base {
|
|
37
|
+
}
|
|
38
|
+
declare const UtilityLlm_base: Context.TagClass<UtilityLlm, "@xandreed/core/UtilityLlm", {
|
|
39
|
+
readonly complete: (prompt: string) => Effect.Effect<UtilityCompletion, UtilityError>;
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* One-shot helper completions OFF the agent loop (session titles, digests,
|
|
43
|
+
* quick judgments) — backed by the fast-role model so a helper call is cheap
|
|
44
|
+
* and can never park a turn. Never a substitute for the loop's LanguageModel.
|
|
45
|
+
*/
|
|
46
|
+
export declare class UtilityLlm extends UtilityLlm_base {
|
|
47
|
+
}
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Context, Schema } from "effect";
|
|
2
|
+
import { TokenUsage } from "../domain/token-usage.entity.js";
|
|
3
|
+
|
|
4
|
+
export class UtilityError extends Schema.TaggedError()("UtilityError", {
|
|
5
|
+
message: Schema.String
|
|
6
|
+
}) {
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class UtilityCompletion extends Schema.Class("UtilityCompletion")({
|
|
10
|
+
text: Schema.String,
|
|
11
|
+
usage: TokenUsage
|
|
12
|
+
}) {
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class UtilityLlm extends Context.Tag("@xandreed/core/UtilityLlm")() {
|
|
16
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Effect, Stream } from "effect";
|
|
2
|
+
import type { ConversationId } from "../domain/message.entity.js";
|
|
3
|
+
/**
|
|
4
|
+
* The session chassis — the one runtime shape every agent driver needs, over
|
|
5
|
+
* ONE persisted conversation and an append-only in-memory event ledger:
|
|
6
|
+
*
|
|
7
|
+
* - `send` runs a whole agent turn and SERIALIZES: a second send while a
|
|
8
|
+
* turn runs waits its turn (drivers coalesce user actions before here).
|
|
9
|
+
* - `interrupt` stops the in-flight turn's fiber; the ledger survives.
|
|
10
|
+
* - `subscribe(since)` replays the ledger from a cursor, then streams live,
|
|
11
|
+
* deduped by `seq` (the identity) — a client reconnecting mid-turn gets
|
|
12
|
+
* each event exactly once.
|
|
13
|
+
*
|
|
14
|
+
* Generic over the event vocabulary: a product composes `LoopEvent` with its
|
|
15
|
+
* own `{ type: ... }` events and hands the chassis a `runTurn` that publishes
|
|
16
|
+
* through the given sink. Turn failures and defects are CONTAINED: they
|
|
17
|
+
* become an event (via `onError`), never a dead session.
|
|
18
|
+
*/
|
|
19
|
+
export interface SeqEvent<E> {
|
|
20
|
+
readonly seq: number;
|
|
21
|
+
readonly event: E;
|
|
22
|
+
}
|
|
23
|
+
export interface Session<E> {
|
|
24
|
+
readonly conversationId: ConversationId;
|
|
25
|
+
/** Run one agent turn (serialized with any in-flight turn). */
|
|
26
|
+
readonly send: (text: string) => Effect.Effect<void>;
|
|
27
|
+
/** Interrupt the in-flight turn, if any (idempotent). */
|
|
28
|
+
readonly interrupt: Effect.Effect<void>;
|
|
29
|
+
/** The full ledger + the next cursor (subscribe from here for live-only). */
|
|
30
|
+
readonly state: Effect.Effect<{
|
|
31
|
+
readonly log: ReadonlyArray<SeqEvent<E>>;
|
|
32
|
+
readonly cursor: number;
|
|
33
|
+
}>;
|
|
34
|
+
/** Every event with `seq >= since`: the replay prefix, then live. */
|
|
35
|
+
readonly subscribe: (since: number) => Stream.Stream<SeqEvent<E>>;
|
|
36
|
+
/**
|
|
37
|
+
* The live-only lossy channel: events `isTransient` claimed (token
|
|
38
|
+
* deltas). Never ledgered, never replayed, sliding — safe to drop because
|
|
39
|
+
* every transient fact is restated by a ledgered finalizer. Empty forever
|
|
40
|
+
* when no `isTransient` was given.
|
|
41
|
+
*/
|
|
42
|
+
readonly transient: Stream.Stream<E>;
|
|
43
|
+
/** Interrupt the in-flight turn — the process-exit finalizer. */
|
|
44
|
+
readonly shutdown: Effect.Effect<void>;
|
|
45
|
+
}
|
|
46
|
+
export declare const makeSession: <E, RS = never>(args: {
|
|
47
|
+
readonly conversationId: ConversationId;
|
|
48
|
+
/** Run one turn, publishing events through the sink. Errors/defects are
|
|
49
|
+
* contained by the chassis via `onError`. */
|
|
50
|
+
readonly runTurn: (text: string, publish: (event: E) => Effect.Effect<void>) => Effect.Effect<void, unknown, RS>;
|
|
51
|
+
/** Map a turn failure into the product's error event. */
|
|
52
|
+
readonly onError: (message: string) => E;
|
|
53
|
+
/** The event an INTERRUPTED turn leaves on the ledger — without one the
|
|
54
|
+
* ledger simply stops mid-turn and every driver has to say so by hand. */
|
|
55
|
+
readonly onInterrupt?: () => E;
|
|
56
|
+
/** Route matching events to the lossy `transient` channel INSTEAD of the
|
|
57
|
+
* ledger — a delta flood must never grow the replay log. */
|
|
58
|
+
readonly isTransient?: (event: E) => boolean;
|
|
59
|
+
}) => Effect.Effect<Session<E>, never, RS>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Effect, Exit, Fiber, Option, PubSub, Ref, Stream } from "effect";
|
|
2
|
+
export const makeSession = (args) => Effect.gen(function* () {
|
|
3
|
+
const context = yield* Effect.context(), log = yield* Ref.make([]), hub = yield* PubSub.sliding(512), transientHub = yield* PubSub.sliding(128), running = yield* Ref.make(Option.none()), gate = yield* Effect.makeSemaphore(1), publish = (event) => args.isTransient?.(event) === !0 ? PubSub.publish(transientHub, event).pipe(Effect.asVoid) : Ref.modify(log, (entries) => {
|
|
4
|
+
const entry = { seq: entries.length, event };
|
|
5
|
+
return [entry, [...entries, entry]];
|
|
6
|
+
}).pipe(Effect.flatMap((entry) => PubSub.publish(hub, entry)), Effect.asVoid), runContained = (text) => args.runTurn(text, publish).pipe(Effect.provide(context), Effect.catchAll((error) => publish(args.onError(String(error)))), Effect.catchAllDefect((defect) => publish(args.onError(`turn crashed: ${String(defect)}`)))), send = (text) => gate.withPermits(1)(Effect.gen(function* () {
|
|
7
|
+
const fiber = yield* Effect.fork(runContained(text));
|
|
8
|
+
yield* Ref.set(running, Option.some(fiber));
|
|
9
|
+
yield* Fiber.await(fiber).pipe(Effect.asVoid);
|
|
10
|
+
yield* Ref.set(running, Option.none());
|
|
11
|
+
})), interrupt = Ref.get(running).pipe(Effect.flatMap(Option.match({
|
|
12
|
+
onNone: () => Effect.void,
|
|
13
|
+
onSome: (fiber) => Fiber.interrupt(fiber).pipe(Effect.flatMap((exit) => Exit.isInterrupted(exit) && args.onInterrupt !== void 0 ? publish(args.onInterrupt()) : Effect.void))
|
|
14
|
+
})));
|
|
15
|
+
return {
|
|
16
|
+
conversationId: args.conversationId,
|
|
17
|
+
send,
|
|
18
|
+
interrupt,
|
|
19
|
+
state: Ref.get(log).pipe(Effect.map((entries) => ({ log: entries, cursor: entries.length }))),
|
|
20
|
+
subscribe: (since) => Stream.unwrapScoped(Effect.gen(function* () {
|
|
21
|
+
const sub = yield* PubSub.subscribe(hub), entries = yield* Ref.get(log), replay = Stream.fromIterable(entries.filter((e) => e.seq >= since)), seenThrough = entries.length, live = Stream.fromQueue(sub).pipe(Stream.filter((e) => e.seq >= Math.max(since, seenThrough)));
|
|
22
|
+
return Stream.concat(replay, live);
|
|
23
|
+
})),
|
|
24
|
+
transient: Stream.fromPubSub(transientHub),
|
|
25
|
+
shutdown: interrupt
|
|
26
|
+
};
|
|
27
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Option, Schema } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* The SPEC artifact of the spec-driven pipeline: a rough idea becomes a
|
|
4
|
+
* SpecDoc (drafted by the refiner agent, refined WITH the human, LOCKED by
|
|
5
|
+
* the human), and only a locked spec forges. Persisted as
|
|
6
|
+
* `<workspace>/.efferent/specs/<slug>.md` — git-committable provenance (the
|
|
7
|
+
* persisted `FactoryRun.spec` ties back to it). See `docs/agents/coder.md`.
|
|
8
|
+
*
|
|
9
|
+
* The wire format is markdown: flat YAML-ish frontmatter (the machine half —
|
|
10
|
+
* the shared `parseFrontmatter` is flat `key: value` only) + strict sections
|
|
11
|
+
* (the human half): `# Goal`, `## Acceptance`, `## Checks` (`- name: command`
|
|
12
|
+
* bullets — machine-checkable acceptance, which drivers turn into rank-2
|
|
13
|
+
* command gates), `## Constraints`, `## Non-goals`. The codec lives in
|
|
14
|
+
* `usecases/specCodec.ts` and round-trips deterministically.
|
|
15
|
+
*/
|
|
16
|
+
/** Kebab-case spec identity — the file's basename, minted once. */
|
|
17
|
+
export declare const SpecSlug: Schema.brand<Schema.filter<typeof Schema.String>, "SpecSlug">;
|
|
18
|
+
export type SpecSlug = typeof SpecSlug.Type;
|
|
19
|
+
export declare const SpecStatus: Schema.Literal<["draft", "locked"]>;
|
|
20
|
+
export type SpecStatus = typeof SpecStatus.Type;
|
|
21
|
+
declare const SpecCheck_base: Schema.Class<SpecCheck, {
|
|
22
|
+
name: Schema.filter<typeof Schema.String>;
|
|
23
|
+
command: typeof Schema.NonEmptyString;
|
|
24
|
+
}, Schema.Struct.Encoded<{
|
|
25
|
+
name: Schema.filter<typeof Schema.String>;
|
|
26
|
+
command: typeof Schema.NonEmptyString;
|
|
27
|
+
}>, never, {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
} & {
|
|
30
|
+
readonly command: string;
|
|
31
|
+
}, {}, {}>;
|
|
32
|
+
/** A machine-checkable acceptance criterion: a named shell command that must
|
|
33
|
+
* exit 0. Drivers append one rank-2 command gate per check (`accept:<name>`). */
|
|
34
|
+
export declare class SpecCheck extends SpecCheck_base {
|
|
35
|
+
}
|
|
36
|
+
declare const SpecLimits_base: Schema.Class<SpecLimits, {
|
|
37
|
+
maxAttempts: Schema.filter<typeof Schema.Int>;
|
|
38
|
+
budgetMinutes: typeof Schema.Positive;
|
|
39
|
+
}, Schema.Struct.Encoded<{
|
|
40
|
+
maxAttempts: Schema.filter<typeof Schema.Int>;
|
|
41
|
+
budgetMinutes: typeof Schema.Positive;
|
|
42
|
+
}>, never, {
|
|
43
|
+
readonly maxAttempts: number;
|
|
44
|
+
} & {
|
|
45
|
+
readonly budgetMinutes: number;
|
|
46
|
+
}, {}, {}>;
|
|
47
|
+
/** Forge-loop bounds (mirrors foundry's ForgeLimits, in human units). */
|
|
48
|
+
export declare class SpecLimits extends SpecLimits_base {
|
|
49
|
+
}
|
|
50
|
+
declare const SpecGates_base: Schema.Class<SpecGates, {
|
|
51
|
+
config: Schema.optionalWith<typeof Schema.NonEmptyString, {
|
|
52
|
+
as: "Option";
|
|
53
|
+
}>;
|
|
54
|
+
testCommand: Schema.optionalWith<typeof Schema.NonEmptyString, {
|
|
55
|
+
as: "Option";
|
|
56
|
+
}>;
|
|
57
|
+
noTest: Schema.optionalWith<typeof Schema.Boolean, {
|
|
58
|
+
default: () => false;
|
|
59
|
+
}>;
|
|
60
|
+
/** The rank-4 LLM judge — ON by default; a spec opts out with `judge: false`. */
|
|
61
|
+
judge: Schema.optionalWith<typeof Schema.Boolean, {
|
|
62
|
+
default: () => true;
|
|
63
|
+
}>;
|
|
64
|
+
}, Schema.Struct.Encoded<{
|
|
65
|
+
config: Schema.optionalWith<typeof Schema.NonEmptyString, {
|
|
66
|
+
as: "Option";
|
|
67
|
+
}>;
|
|
68
|
+
testCommand: Schema.optionalWith<typeof Schema.NonEmptyString, {
|
|
69
|
+
as: "Option";
|
|
70
|
+
}>;
|
|
71
|
+
noTest: Schema.optionalWith<typeof Schema.Boolean, {
|
|
72
|
+
default: () => false;
|
|
73
|
+
}>;
|
|
74
|
+
/** The rank-4 LLM judge — ON by default; a spec opts out with `judge: false`. */
|
|
75
|
+
judge: Schema.optionalWith<typeof Schema.Boolean, {
|
|
76
|
+
default: () => true;
|
|
77
|
+
}>;
|
|
78
|
+
}>, never, {
|
|
79
|
+
readonly config: Option.Option<string>;
|
|
80
|
+
} & {
|
|
81
|
+
readonly testCommand: Option.Option<string>;
|
|
82
|
+
} & {
|
|
83
|
+
readonly noTest?: boolean;
|
|
84
|
+
} & {
|
|
85
|
+
readonly judge?: boolean;
|
|
86
|
+
}, {}, {}>;
|
|
87
|
+
/** Gate-suite selection overrides; absent fields fall through to the driver's
|
|
88
|
+
* workspace discovery (precedence: CLI flags > these > discovery). */
|
|
89
|
+
export declare class SpecGates extends SpecGates_base {
|
|
90
|
+
}
|
|
91
|
+
declare const SpecDoc_base: Schema.Class<SpecDoc, {
|
|
92
|
+
slug: Schema.brand<Schema.filter<typeof Schema.String>, "SpecSlug">;
|
|
93
|
+
status: Schema.Literal<["draft", "locked"]>;
|
|
94
|
+
/** ISO timestamps — strings on purpose (stable round-trip, no Date drift). */
|
|
95
|
+
created: typeof Schema.NonEmptyString;
|
|
96
|
+
locked: Schema.optionalWith<typeof Schema.NonEmptyString, {
|
|
97
|
+
as: "Option";
|
|
98
|
+
}>;
|
|
99
|
+
/** One imperative paragraph — becomes foundry `Spec.goal`. */
|
|
100
|
+
goal: typeof Schema.NonEmptyString;
|
|
101
|
+
/** Rendered verbatim into the implementor brief; also foundry `Spec.acceptance`. */
|
|
102
|
+
acceptance: Schema.Array$<typeof Schema.NonEmptyString>;
|
|
103
|
+
/** Hard boundaries for the implementor (brief-only; never enter foundry's Spec). */
|
|
104
|
+
constraints: Schema.Array$<typeof Schema.NonEmptyString>;
|
|
105
|
+
/** Explicit scope fences (brief-only). */
|
|
106
|
+
nonGoals: Schema.Array$<typeof Schema.NonEmptyString>;
|
|
107
|
+
checks: Schema.Array$<typeof SpecCheck>;
|
|
108
|
+
limits: typeof SpecLimits;
|
|
109
|
+
gates: typeof SpecGates;
|
|
110
|
+
}, Schema.Struct.Encoded<{
|
|
111
|
+
slug: Schema.brand<Schema.filter<typeof Schema.String>, "SpecSlug">;
|
|
112
|
+
status: Schema.Literal<["draft", "locked"]>;
|
|
113
|
+
/** ISO timestamps — strings on purpose (stable round-trip, no Date drift). */
|
|
114
|
+
created: typeof Schema.NonEmptyString;
|
|
115
|
+
locked: Schema.optionalWith<typeof Schema.NonEmptyString, {
|
|
116
|
+
as: "Option";
|
|
117
|
+
}>;
|
|
118
|
+
/** One imperative paragraph — becomes foundry `Spec.goal`. */
|
|
119
|
+
goal: typeof Schema.NonEmptyString;
|
|
120
|
+
/** Rendered verbatim into the implementor brief; also foundry `Spec.acceptance`. */
|
|
121
|
+
acceptance: Schema.Array$<typeof Schema.NonEmptyString>;
|
|
122
|
+
/** Hard boundaries for the implementor (brief-only; never enter foundry's Spec). */
|
|
123
|
+
constraints: Schema.Array$<typeof Schema.NonEmptyString>;
|
|
124
|
+
/** Explicit scope fences (brief-only). */
|
|
125
|
+
nonGoals: Schema.Array$<typeof Schema.NonEmptyString>;
|
|
126
|
+
checks: Schema.Array$<typeof SpecCheck>;
|
|
127
|
+
limits: typeof SpecLimits;
|
|
128
|
+
gates: typeof SpecGates;
|
|
129
|
+
}>, never, {
|
|
130
|
+
readonly status: "draft" | "locked";
|
|
131
|
+
} & {
|
|
132
|
+
readonly locked: Option.Option<string>;
|
|
133
|
+
} & {
|
|
134
|
+
readonly slug: string & import("effect/Brand").Brand<"SpecSlug">;
|
|
135
|
+
} & {
|
|
136
|
+
readonly created: string;
|
|
137
|
+
} & {
|
|
138
|
+
readonly goal: string;
|
|
139
|
+
} & {
|
|
140
|
+
readonly limits: SpecLimits;
|
|
141
|
+
} & {
|
|
142
|
+
readonly gates: SpecGates;
|
|
143
|
+
} & {
|
|
144
|
+
readonly acceptance: readonly string[];
|
|
145
|
+
} & {
|
|
146
|
+
readonly constraints: readonly string[];
|
|
147
|
+
} & {
|
|
148
|
+
readonly nonGoals: readonly string[];
|
|
149
|
+
} & {
|
|
150
|
+
readonly checks: readonly SpecCheck[];
|
|
151
|
+
}, {}, {}>;
|
|
152
|
+
export declare class SpecDoc extends SpecDoc_base {
|
|
153
|
+
}
|
|
154
|
+
export declare const DEFAULT_SPEC_LIMITS: SpecLimits;
|
|
155
|
+
/**
|
|
156
|
+
* The prompt section a spec-carrying session renders each turn — the
|
|
157
|
+
* deterministic successor of the old Directive section: the agent works TO
|
|
158
|
+
* the acceptance criteria, and deterministic gates (not an LLM judge) decide
|
|
159
|
+
* whether the work is done.
|
|
160
|
+
*/
|
|
161
|
+
export declare const renderSpecSection: (doc: SpecDoc) => string;
|
|
162
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Option, Schema } from "effect";
|
|
2
|
+
export const SpecSlug = Schema.String.pipe(Schema.pattern(/^[a-z0-9][a-z0-9-]*$/), Schema.brand("SpecSlug")), SpecStatus = Schema.Literal("draft", "locked");
|
|
3
|
+
|
|
4
|
+
export class SpecCheck extends Schema.Class("SpecCheck")({
|
|
5
|
+
name: Schema.String.pipe(Schema.pattern(/^[a-z0-9][a-z0-9-]*$/)),
|
|
6
|
+
command: Schema.NonEmptyString
|
|
7
|
+
}) {
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class SpecLimits extends Schema.Class("SpecLimits")({
|
|
11
|
+
maxAttempts: Schema.Int.pipe(Schema.between(1, 10)),
|
|
12
|
+
budgetMinutes: Schema.Positive
|
|
13
|
+
}) {
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class SpecGates extends Schema.Class("SpecGates")({
|
|
17
|
+
config: Schema.optionalWith(Schema.NonEmptyString, { as: "Option" }),
|
|
18
|
+
testCommand: Schema.optionalWith(Schema.NonEmptyString, { as: "Option" }),
|
|
19
|
+
noTest: Schema.optionalWith(Schema.Boolean, { default: () => !1 }),
|
|
20
|
+
judge: Schema.optionalWith(Schema.Boolean, { default: () => !0 })
|
|
21
|
+
}) {
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class SpecDoc extends Schema.Class("SpecDoc")({
|
|
25
|
+
slug: SpecSlug,
|
|
26
|
+
status: SpecStatus,
|
|
27
|
+
created: Schema.NonEmptyString,
|
|
28
|
+
locked: Schema.optionalWith(Schema.NonEmptyString, { as: "Option" }),
|
|
29
|
+
goal: Schema.NonEmptyString,
|
|
30
|
+
acceptance: Schema.Array(Schema.NonEmptyString),
|
|
31
|
+
constraints: Schema.Array(Schema.NonEmptyString),
|
|
32
|
+
nonGoals: Schema.Array(Schema.NonEmptyString),
|
|
33
|
+
checks: Schema.Array(SpecCheck),
|
|
34
|
+
limits: SpecLimits,
|
|
35
|
+
gates: SpecGates
|
|
36
|
+
}) {
|
|
37
|
+
}
|
|
38
|
+
export const DEFAULT_SPEC_LIMITS = new SpecLimits({ maxAttempts: 3, budgetMinutes: 15 }), renderSpecSection = (doc) => {
|
|
39
|
+
const bullets = (items) => items.map((item) => `- ${item}`).join(`
|
|
40
|
+
`), acceptance = doc.acceptance.length === 0 ? "" : `
|
|
41
|
+
|
|
42
|
+
## Acceptance
|
|
43
|
+
${bullets(doc.acceptance)}`, checks = doc.checks.length === 0 ? "" : `
|
|
44
|
+
|
|
45
|
+
## Machine checks (deterministic gates \u2014 these MUST pass)
|
|
46
|
+
${doc.checks.map((check) => `- ${check.name}: \`${check.command}\``).join(`
|
|
47
|
+
`)}`, constraints = doc.constraints.length === 0 ? "" : `
|
|
48
|
+
|
|
49
|
+
## Constraints
|
|
50
|
+
${bullets(doc.constraints)}`, nonGoals = doc.nonGoals.length === 0 ? "" : `
|
|
51
|
+
|
|
52
|
+
## Non-goals
|
|
53
|
+
${bullets(doc.nonGoals)}`;
|
|
54
|
+
return `# Spec (${doc.status}): ${doc.slug}
|
|
55
|
+
|
|
56
|
+
${doc.goal}${acceptance}${checks}${constraints}${nonGoals}
|
|
57
|
+
|
|
58
|
+
Work to this spec. The acceptance criteria are the contract; deterministic gates judge the workspace \u2014 the spec is done when the gates say so, not when the work feels finished.`;
|
|
59
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SpecDoc } from "./SpecDoc.js";
|
|
3
|
+
import type { SpecSlug } from "./SpecDoc.js";
|
|
4
|
+
declare const SpecDocParseError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
5
|
+
readonly _tag: "SpecDocParseError";
|
|
6
|
+
} & Readonly<A>;
|
|
7
|
+
/** A SpecDoc file that doesn't decode — names the exact violation. */
|
|
8
|
+
export declare class SpecDocParseError extends SpecDocParseError_base<{
|
|
9
|
+
readonly message: string;
|
|
10
|
+
}> {
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Decode a SpecDoc file's text. The slug comes from the FILE NAME (the file
|
|
14
|
+
* owns identity — it is not repeated in the frontmatter). Round-trip law:
|
|
15
|
+
* `decodeSpecDocText(slug, encodeSpecDocText(doc)) ≡ doc`.
|
|
16
|
+
*/
|
|
17
|
+
export declare const decodeSpecDocText: (slug: string, text: string) => Effect.Effect<SpecDoc, SpecDocParseError>;
|
|
18
|
+
/** Prose bullets are single-line BY GRAMMAR — collapse any whitespace runs
|
|
19
|
+
* (incl. newlines) a model slipped in. Safe for prose; NEVER applied to
|
|
20
|
+
* check commands (semantics — the propose handler rejects those instead). */
|
|
21
|
+
export declare const normalizeBullet: (text: string) => string;
|
|
22
|
+
/** Deterministic inverse of {@link decodeSpecDocText}: stable key order,
|
|
23
|
+
* canonical section order, absent options omitted. Prose bullets are
|
|
24
|
+
* normalized to keep the encode inside its own grammar (round-trip law). */
|
|
25
|
+
export declare const encodeSpecDocText: (doc: SpecDoc) => string;
|
|
26
|
+
/** Deterministic kebab slug from a goal/idea (mint once; the file owns it). */
|
|
27
|
+
export declare const specSlug: (goal: string) => SpecSlug;
|
|
28
|
+
/** First-wins collision suffixing over an existing-name predicate. */
|
|
29
|
+
export declare const uniqueSlug: (base: SpecSlug, taken: (slug: string) => boolean) => SpecSlug;
|
|
30
|
+
export {};
|