@xandreed/core 0.4.0 → 0.11.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/README.md +1 -1
- package/dist/conformance.entity.d.ts +15 -0
- package/dist/conformance.entity.js +7 -0
- package/dist/decision-record.entity.d.ts +23 -23
- package/dist/decision-record.entity.functions.d.ts +6 -0
- package/dist/decision-record.entity.functions.js +3 -0
- package/dist/decision-record.entity.js +4 -4
- package/dist/domain/agent-failure.entity.d.ts +6 -6
- package/dist/domain/agent-failure.entity.js +12 -1
- package/dist/domain/failure.entity.d.ts +2 -2
- package/dist/domain/loop-event.entity.d.ts +2 -0
- package/dist/domain/message.entity.d.ts +143 -156
- package/dist/domain/message.entity.js +6 -6
- package/dist/domain/model-call-policy.entity.d.ts +3 -3
- package/dist/domain/model-call-policy.entity.js +1 -1
- package/dist/domain/model-catalog.entity.d.ts +4 -4
- package/dist/domain/model-catalog.entity.js +1 -1
- package/dist/domain/model-selection.entity.d.ts +6 -13
- package/dist/domain/prompt-provenance.entity.d.ts +6 -6
- package/dist/domain/prompt-provenance.entity.js +4 -4
- package/dist/domain/token-usage.entity.d.ts +4 -4
- package/dist/harness/capability.entity.d.ts +88 -41
- package/dist/harness/capability.entity.functions.d.ts +30 -1
- package/dist/harness/capability.entity.functions.js +55 -2
- package/dist/harness/capability.entity.js +33 -12
- package/dist/harness/config.entity.d.ts +28 -28
- package/dist/harness/config.entity.js +4 -4
- package/dist/harness/plugin-stack.adapter.d.ts +24 -0
- package/dist/harness/plugin-stack.adapter.js +8 -0
- package/dist/harness/plugin-stack.adapter.test.d.ts +1 -0
- package/dist/harness/plugin.adapter.d.ts +15 -4
- package/dist/harness/plugin.adapter.js +18 -11
- package/dist/harness/plugin.adapter.test.d.ts +1 -0
- package/dist/harness/plugin.entity.d.ts +26 -10
- package/dist/harness/plugin.entity.js +1 -1
- package/dist/harness/session.entity.d.ts +20 -20
- package/dist/harness/session.entity.js +1 -1
- package/dist/harness/skill-files.d.ts +17 -0
- package/dist/harness/skill-files.js +25 -0
- package/dist/index.d.ts +47 -4
- package/dist/index.js +45 -4
- package/dist/loop/cacheKey.d.ts +3 -3
- package/dist/loop/cacheKey.js +4 -2
- package/dist/loop/mapping.d.ts +4 -5
- package/dist/loop/mapping.js +8 -16
- package/dist/loop/modelPolicy.d.ts +4 -4
- package/dist/loop/modelPolicy.js +2 -2
- package/dist/loop/promptProvenance.d.ts +2 -2
- package/dist/loop/promptProvenance.js +2 -2
- package/dist/loop/stepContext.d.ts +2 -2
- package/dist/loop/stepContext.js +2 -2
- package/dist/loop/streamFold.js +3 -3
- package/dist/loop/toolSchema.d.ts +17 -0
- package/dist/loop/toolSchema.js +25 -0
- package/dist/loop/toolSchema.test.d.ts +1 -0
- package/dist/mcp/bridge.d.ts +15 -15
- package/dist/mcp/bridge.js +14 -14
- package/dist/memory/memory-log.entity.d.ts +320 -0
- package/dist/memory/memory-log.entity.functions.d.ts +89 -0
- package/dist/memory/memory-log.entity.functions.js +180 -0
- package/dist/memory/memory-log.entity.js +75 -0
- package/dist/memory/memory-log.entity.test.d.ts +1 -0
- package/dist/memory/memory-session.d.ts +51 -0
- package/dist/memory/memory-session.js +97 -0
- package/dist/memory/memory-session.test.d.ts +1 -0
- package/dist/memory/memory.conformance.d.ts +32 -0
- package/dist/memory/memory.conformance.js +102 -0
- package/dist/ports/auth-store.port.d.ts +16 -18
- package/dist/ports/auth-store.port.js +13 -9
- package/dist/ports/capability.port.d.ts +131 -7
- package/dist/ports/capability.port.js +2 -2
- package/dist/ports/conversation-store.port.d.ts +54 -177
- package/dist/ports/conversation-store.port.js +9 -9
- package/dist/ports/file-system.port.d.ts +5 -7
- package/dist/ports/file-system.port.js +1 -1
- package/dist/ports/harness.port.d.ts +19 -13
- package/dist/ports/harness.port.js +10 -10
- package/dist/ports/mcp-client.port.d.ts +15 -47
- package/dist/ports/mcp-client.port.js +3 -3
- package/dist/ports/memory.port.d.ts +116 -0
- package/dist/ports/memory.port.js +7 -0
- package/dist/ports/model-catalog.port.d.ts +1 -1
- package/dist/ports/model-catalog.port.js +1 -1
- package/dist/ports/permission.port.d.ts +10 -0
- package/dist/ports/permission.port.js +4 -0
- package/dist/ports/run-context.port.d.ts +28 -0
- package/dist/ports/run-context.port.js +4 -0
- package/dist/ports/session-log.port.d.ts +25 -0
- package/dist/ports/session-log.port.js +4 -0
- package/dist/ports/sessions.port.d.ts +141 -0
- package/dist/ports/sessions.port.js +4 -0
- package/dist/ports/settings-store.port.d.ts +18 -56
- package/dist/ports/settings-store.port.js +3 -3
- package/dist/ports/shell.port.d.ts +9 -21
- package/dist/ports/shell.port.js +1 -1
- package/dist/ports/step-loop.port.d.ts +85 -0
- package/dist/ports/step-loop.port.js +4 -0
- package/dist/ports/tasks.port.d.ts +59 -0
- package/dist/ports/tasks.port.js +10 -0
- package/dist/ports/tool-registry.port.d.ts +53 -0
- package/dist/ports/tool-registry.port.js +4 -0
- package/dist/ports/turn-admission.port.d.ts +23 -0
- package/dist/ports/turn-admission.port.js +4 -0
- package/dist/ports/turn-events.port.d.ts +55 -0
- package/dist/ports/turn-events.port.js +7 -0
- package/dist/ports/turn-scope.port.d.ts +101 -0
- package/dist/ports/turn-scope.port.js +10 -0
- package/dist/ports/turn.port.d.ts +113 -0
- package/dist/ports/turn.port.js +0 -0
- package/dist/ports/utility-llm.port.d.ts +12 -31
- package/dist/ports/utility-llm.port.js +1 -1
- package/dist/session/chassis.js +7 -7
- package/dist/session/session-event.entity.d.ts +55 -0
- package/dist/session/session-event.entity.functions.d.ts +31 -0
- package/dist/session/session-event.entity.functions.js +42 -0
- package/dist/session/session-event.entity.js +37 -0
- package/dist/session/session-event.entity.test.d.ts +1 -0
- package/dist/session/session-log.conformance.d.ts +13 -0
- package/dist/session/session-log.conformance.js +167 -0
- package/dist/session/session-log.entity.d.ts +164 -0
- package/dist/session/session-log.entity.js +82 -0
- package/dist/session/session-log.memory.adapter.d.ts +7 -0
- package/dist/session/session-log.memory.adapter.js +82 -0
- package/dist/session/session-log.memory.adapter.test.d.ts +1 -0
- package/dist/session/sessions.entity.d.ts +201 -0
- package/dist/session/sessions.entity.js +98 -0
- package/dist/session/tasks.entity.d.ts +39 -0
- package/dist/session/tasks.entity.js +26 -0
- package/dist/session/turn-admission.open.adapter.d.ts +4 -0
- package/dist/session/turn-admission.open.adapter.js +3 -0
- package/dist/spec/SpecDoc.d.ts +30 -117
- package/dist/spec/SpecDoc.js +10 -10
- package/dist/spec/codec.js +1 -1
- package/dist/turn/model-request.entity.d.ts +99 -0
- package/dist/turn/model-request.entity.functions.d.ts +108 -0
- package/dist/turn/model-request.entity.functions.js +167 -0
- package/dist/turn/model-request.entity.js +41 -0
- package/dist/turn/model-request.entity.test.d.ts +1 -0
- package/dist/turn/prompt-sections.d.ts +10 -0
- package/dist/turn/prompt-sections.js +3 -0
- package/dist/turn/step-loop.conformance.d.ts +16 -0
- package/dist/turn/step-loop.conformance.js +164 -0
- package/dist/turn/testing.d.ts +30 -0
- package/dist/turn/turn-bus.d.ts +23 -0
- package/dist/turn/turn-bus.js +54 -0
- package/dist/turn/turn-bus.test.d.ts +1 -0
- package/dist/turn/turn-event.entity.d.ts +263 -0
- package/dist/turn/turn-event.entity.functions.d.ts +35 -0
- package/dist/turn/turn-event.entity.functions.js +21 -0
- package/dist/turn/turn-event.entity.js +101 -0
- package/dist/turn/turn-lifecycle.d.ts +32 -0
- package/dist/turn/turn-lifecycle.js +48 -0
- package/dist/turn/turn-run.d.ts +26 -0
- package/dist/turn/turn-run.js +105 -0
- package/dist/turn/turn.adapter.d.ts +21 -0
- package/dist/turn/turn.adapter.js +126 -0
- package/dist/turn/turn.adapter.test.d.ts +1 -0
- package/dist/turn/turn.conformance.d.ts +20 -0
- package/dist/turn/turn.conformance.js +74 -0
- package/dist/turn/user-message.entity.d.ts +14 -0
- package/dist/turn/user-message.entity.js +10 -0
- package/dist/util/json.d.ts +2 -2
- package/dist/util/json.js +10 -10
- package/package.json +3 -3
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** Public provider options only. Credentials and endpoint authorization never belong here. */
|
|
3
|
+
export declare const ModelRequestDescriptor: Schema.Struct<{
|
|
4
|
+
readonly provider: Schema.String;
|
|
5
|
+
readonly model: Schema.String;
|
|
6
|
+
readonly settings: Schema.$Record<Schema.String, Schema.Unknown>;
|
|
7
|
+
}>;
|
|
8
|
+
export type ModelRequestDescriptor = typeof ModelRequestDescriptor.Type;
|
|
9
|
+
/**
|
|
10
|
+
* The model-visible declaration. A provider-defined tool (one the provider
|
|
11
|
+
* runs) keeps its id and name, and its args only as their key names and the
|
|
12
|
+
* SHA-256 of their canonical JSON: args can carry credentials (a remote tool
|
|
13
|
+
* server's authorization), so their values are never stored. A dispatch
|
|
14
|
+
* compares the digests, so any change of a value still diverges.
|
|
15
|
+
*/
|
|
16
|
+
export declare const ModelRequestTool: Schema.Struct<{
|
|
17
|
+
readonly name: Schema.String;
|
|
18
|
+
readonly description: Schema.String;
|
|
19
|
+
readonly parameters: Schema.Unknown;
|
|
20
|
+
readonly provider: Schema.OptionFromNullOr<Schema.Struct<{
|
|
21
|
+
readonly id: Schema.String;
|
|
22
|
+
readonly name: Schema.String;
|
|
23
|
+
readonly argKeys: Schema.$Array<Schema.String>;
|
|
24
|
+
readonly argsDigest: Schema.String;
|
|
25
|
+
}>>;
|
|
26
|
+
}>;
|
|
27
|
+
export type ModelRequestTool = typeof ModelRequestTool.Type;
|
|
28
|
+
/** A saved pure-render recipe: replay uses these options even after a memory plugin upgrade. */
|
|
29
|
+
export declare const MemoryRenderRecipe: Schema.Struct<{
|
|
30
|
+
readonly strategy: Schema.String;
|
|
31
|
+
readonly currentTurn: Schema.Int;
|
|
32
|
+
readonly currentRun: Schema.String;
|
|
33
|
+
readonly turnContext: Schema.Literals<readonly ["current", "all"]>;
|
|
34
|
+
readonly replies: Schema.Boolean;
|
|
35
|
+
readonly stepContext: Schema.Literals<readonly ["tail", "none"]>;
|
|
36
|
+
readonly digests: Schema.Boolean;
|
|
37
|
+
readonly media: Schema.Struct<{
|
|
38
|
+
readonly mode: Schema.Literals<readonly ["none", "inline"]>;
|
|
39
|
+
readonly maxImages: Schema.Int;
|
|
40
|
+
}>;
|
|
41
|
+
}>;
|
|
42
|
+
export type MemoryRenderRecipe = typeof MemoryRenderRecipe.Type;
|
|
43
|
+
/**
|
|
44
|
+
* Durable protocol, separate from diagnostic context/model events. Messages
|
|
45
|
+
* remain in memory events: the request is rebuilt from the memory entries up
|
|
46
|
+
* to `through`, the last one its build folded, with the saved `render`
|
|
47
|
+
* recipe, so a memory write recorded after the build (a reaction to
|
|
48
|
+
* `context.built`) is not part of it. Each header is self-contained: it
|
|
49
|
+
* repeats the full system text and every active tool's declaration, a
|
|
50
|
+
* deliberate storage trade-off so one event (and the memory before it)
|
|
51
|
+
* rebuilds a request without reading other headers.
|
|
52
|
+
*/
|
|
53
|
+
export declare const ModelRequestHeader: Schema.Struct<{
|
|
54
|
+
readonly version: Schema.Literal<1>;
|
|
55
|
+
readonly runId: Schema.String;
|
|
56
|
+
readonly step: Schema.Int;
|
|
57
|
+
readonly strategyVersion: Schema.String;
|
|
58
|
+
readonly system: Schema.String;
|
|
59
|
+
readonly render: Schema.Struct<{
|
|
60
|
+
readonly strategy: Schema.String;
|
|
61
|
+
readonly currentTurn: Schema.Int;
|
|
62
|
+
readonly currentRun: Schema.String;
|
|
63
|
+
readonly turnContext: Schema.Literals<readonly ["current", "all"]>;
|
|
64
|
+
readonly replies: Schema.Boolean;
|
|
65
|
+
readonly stepContext: Schema.Literals<readonly ["tail", "none"]>;
|
|
66
|
+
readonly digests: Schema.Boolean;
|
|
67
|
+
readonly media: Schema.Struct<{
|
|
68
|
+
readonly mode: Schema.Literals<readonly ["none", "inline"]>;
|
|
69
|
+
readonly maxImages: Schema.Int;
|
|
70
|
+
}>;
|
|
71
|
+
}>;
|
|
72
|
+
/** The last memory entry the build included (its cut); none for an empty log. */
|
|
73
|
+
readonly through: Schema.OptionFromNullOr<Schema.brand<Schema.Trimmed, "EntryId">>;
|
|
74
|
+
readonly contextFingerprint: Schema.String;
|
|
75
|
+
readonly tools: Schema.$Array<Schema.Struct<{
|
|
76
|
+
readonly name: Schema.String;
|
|
77
|
+
readonly description: Schema.String;
|
|
78
|
+
readonly parameters: Schema.Unknown;
|
|
79
|
+
readonly provider: Schema.OptionFromNullOr<Schema.Struct<{
|
|
80
|
+
readonly id: Schema.String;
|
|
81
|
+
readonly name: Schema.String;
|
|
82
|
+
readonly argKeys: Schema.$Array<Schema.String>;
|
|
83
|
+
readonly argsDigest: Schema.String;
|
|
84
|
+
}>>;
|
|
85
|
+
}>>;
|
|
86
|
+
readonly toolChoice: Schema.Unknown;
|
|
87
|
+
/** None explicitly marks an opaque model whose provider configuration cannot be inspected. */
|
|
88
|
+
readonly model: Schema.OptionFromNullOr<Schema.Struct<{
|
|
89
|
+
readonly provider: Schema.String;
|
|
90
|
+
readonly model: Schema.String;
|
|
91
|
+
readonly settings: Schema.$Record<Schema.String, Schema.Unknown>;
|
|
92
|
+
}>>;
|
|
93
|
+
readonly cacheKey: Schema.OptionFromNullOr<Schema.String>;
|
|
94
|
+
readonly callPolicy: Schema.OptionFromNullOr<Schema.Struct<{
|
|
95
|
+
readonly effort: Schema.Literals<readonly ["none", "low", "medium", "high", "xhigh", "max"]>;
|
|
96
|
+
readonly maxOutputTokens: Schema.optional<Schema.Number>;
|
|
97
|
+
}>>;
|
|
98
|
+
}>;
|
|
99
|
+
export type ModelRequestHeader = typeof ModelRequestHeader.Type;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { Prompt, Tool } from "effect/ai";
|
|
3
|
+
import type { LanguageModel } from "effect/ai";
|
|
4
|
+
import type { ModelCallPolicy } from "../domain/model-call-policy.entity.js";
|
|
5
|
+
import { HarnessError } from "../harness/plugin.entity.js";
|
|
6
|
+
import type { LogEntry } from "../memory/memory-log.entity.js";
|
|
7
|
+
import type { SessionLogEvent } from "../session/session-log.entity.js";
|
|
8
|
+
import { ModelRequestDescriptor, ModelRequestHeader } from "./model-request.entity.js";
|
|
9
|
+
import type { ModelRequestTool } from "./model-request.entity.js";
|
|
10
|
+
/** Attach inspectable public configuration to an otherwise opaque Effect AI model. */
|
|
11
|
+
export declare const describeModel: (model: LanguageModel.LanguageModel, descriptor: ModelRequestDescriptor | Effect.Effect<ModelRequestDescriptor, HarnessError>) => LanguageModel.LanguageModel;
|
|
12
|
+
/** An undescribed model is explicitly opaque; descriptions are checked at each dispatch. */
|
|
13
|
+
export declare const modelRequestDescriptorOf: (model: LanguageModel.LanguageModel) => Effect.Effect<Option.Option<ModelRequestDescriptor>, HarnessError>;
|
|
14
|
+
/**
|
|
15
|
+
* A model that reads its configuration per call (a settings-backed router)
|
|
16
|
+
* resolves it once per step: the model `resolve` returns serves the step's
|
|
17
|
+
* header and every provider attempt of the step (a stream's fallback
|
|
18
|
+
* included), so a configuration change applies from the next step.
|
|
19
|
+
*/
|
|
20
|
+
export declare const resolvingModel: (model: LanguageModel.LanguageModel, resolve: Effect.Effect<LanguageModel.LanguageModel, HarnessError>) => LanguageModel.LanguageModel;
|
|
21
|
+
/** The model one step uses: a resolving model's resolution, any other model itself. */
|
|
22
|
+
export declare const resolveModelRequest: (model: LanguageModel.LanguageModel) => Effect.Effect<LanguageModel.LanguageModel, HarnessError>;
|
|
23
|
+
/** Serialize active tools in activation order, independently at preparation and dispatch. */
|
|
24
|
+
export declare const modelRequestTools: (tools: ReadonlyArray<Tool.Any>) => ReadonlyArray<ModelRequestTool>;
|
|
25
|
+
/** Last header for this run and step, decoded from a fresh durable snapshot. */
|
|
26
|
+
export declare const modelRequestHeaderOf: (events: ReadonlyArray<SessionLogEvent>, runId: string, step: number) => Effect.Effect<ModelRequestHeader, HarnessError>;
|
|
27
|
+
/** A model request as a dispatch check compares it: the Effect AI prompt, and the parts the header declares. */
|
|
28
|
+
export interface ComparedModelRequest {
|
|
29
|
+
readonly prompt: Prompt.Prompt;
|
|
30
|
+
readonly tools: ReadonlyArray<ModelRequestTool>;
|
|
31
|
+
readonly toolChoice: unknown;
|
|
32
|
+
readonly model: Option.Option<ModelRequestDescriptor>;
|
|
33
|
+
readonly cacheKey: Option.Option<string>;
|
|
34
|
+
readonly callPolicy: Option.Option<ModelCallPolicy>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Rebuild a request from its header and memory facts: the entries up to the
|
|
38
|
+
* header's cut, folded with its saved render recipe, never with current
|
|
39
|
+
* plugin definitions. Fails with `request.diverged` (`context`) when they do
|
|
40
|
+
* not rebuild the context the header was prepared from (a fork's own log
|
|
41
|
+
* without its parent's history, say).
|
|
42
|
+
*/
|
|
43
|
+
export declare const reconstructModelRequest: (header: ModelRequestHeader, entries: ReadonlyArray<LogEntry>) => Effect.Effect<{
|
|
44
|
+
prompt: Prompt.Prompt;
|
|
45
|
+
contextFingerprint: string;
|
|
46
|
+
tools: readonly {
|
|
47
|
+
readonly name: string;
|
|
48
|
+
readonly description: string;
|
|
49
|
+
readonly parameters: unknown;
|
|
50
|
+
readonly provider: Option.Option<{
|
|
51
|
+
readonly id: string;
|
|
52
|
+
readonly name: string;
|
|
53
|
+
readonly argKeys: readonly string[];
|
|
54
|
+
readonly argsDigest: string;
|
|
55
|
+
}>;
|
|
56
|
+
}[];
|
|
57
|
+
toolChoice: unknown;
|
|
58
|
+
model: Option.Option<{
|
|
59
|
+
readonly provider: string;
|
|
60
|
+
readonly model: string;
|
|
61
|
+
readonly settings: {
|
|
62
|
+
readonly [x: string]: unknown;
|
|
63
|
+
};
|
|
64
|
+
}>;
|
|
65
|
+
cacheKey: Option.Option<string>;
|
|
66
|
+
callPolicy: Option.Option<{
|
|
67
|
+
readonly effort: "none" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
68
|
+
readonly maxOutputTokens?: number | undefined;
|
|
69
|
+
}>;
|
|
70
|
+
}, HarnessError, never>;
|
|
71
|
+
/** Reconstruct one historical request from the events up to its header, excluding later responses and turns. */
|
|
72
|
+
export declare const replayModelRequest: (events: ReadonlyArray<SessionLogEvent>, runId: string, step: number) => Effect.Effect<{
|
|
73
|
+
prompt: Prompt.Prompt;
|
|
74
|
+
contextFingerprint: string;
|
|
75
|
+
tools: readonly {
|
|
76
|
+
readonly name: string;
|
|
77
|
+
readonly description: string;
|
|
78
|
+
readonly parameters: unknown;
|
|
79
|
+
readonly provider: Option.Option<{
|
|
80
|
+
readonly id: string;
|
|
81
|
+
readonly name: string;
|
|
82
|
+
readonly argKeys: readonly string[];
|
|
83
|
+
readonly argsDigest: string;
|
|
84
|
+
}>;
|
|
85
|
+
}[];
|
|
86
|
+
toolChoice: unknown;
|
|
87
|
+
model: Option.Option<{
|
|
88
|
+
readonly provider: string;
|
|
89
|
+
readonly model: string;
|
|
90
|
+
readonly settings: {
|
|
91
|
+
readonly [x: string]: unknown;
|
|
92
|
+
};
|
|
93
|
+
}>;
|
|
94
|
+
cacheKey: Option.Option<string>;
|
|
95
|
+
callPolicy: Option.Option<{
|
|
96
|
+
readonly effort: "none" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
97
|
+
readonly maxOutputTokens?: number | undefined;
|
|
98
|
+
}>;
|
|
99
|
+
}, HarnessError, never>;
|
|
100
|
+
/** Freeze JSON message data before handing it to a model wrapper. */
|
|
101
|
+
export declare const freezeModelRequest: <A>(value: A) => A;
|
|
102
|
+
/**
|
|
103
|
+
* Compare canonical model-visible bytes, part by part, not object identity or
|
|
104
|
+
* hashes alone. A divergence fails with `request.diverged`, its message
|
|
105
|
+
* starting with the first part that differs (`system`, `messages`, `tools`,
|
|
106
|
+
* `toolChoice`, `model`, `cacheKey` or `callPolicy`).
|
|
107
|
+
*/
|
|
108
|
+
export declare const checkModelRequest: (expected: ComparedModelRequest, actual: ComparedModelRequest) => Effect.Effect<void, HarnessError>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Effect, Option, Schema } from "effect";
|
|
2
|
+
import { Prompt, Tool } from "effect/ai";
|
|
3
|
+
import { HarnessError } from "../harness/plugin.entity.js";
|
|
4
|
+
import { toPromptMessages } from "../loop/mapping.js";
|
|
5
|
+
import { toolParametersSchema } from "../loop/toolSchema.js";
|
|
6
|
+
import { canonicalJson, buildContext } from "../memory/memory-log.entity.functions.js";
|
|
7
|
+
import { entriesOfEvents } from "../session/session-event.entity.functions.js";
|
|
8
|
+
import { ModelRequestDescriptor, ModelRequestHeader } from "./model-request.entity.js";
|
|
9
|
+
const descriptorKey = "efferent/model-request-descriptor", resolverKey = "efferent/model-request-resolver";
|
|
10
|
+
export const describeModel = (model, descriptor) => {
|
|
11
|
+
return { ...model, [descriptorKey]: Effect.isEffect(descriptor) ? descriptor : Effect.succeed(descriptor) };
|
|
12
|
+
}, modelRequestDescriptorOf = (model) => {
|
|
13
|
+
const candidate = descriptorKey in model ? model[descriptorKey] : void 0;
|
|
14
|
+
return Effect.isEffect(candidate) ? candidate.pipe(Effect.flatMap(Schema.decodeUnknownEffect(ModelRequestDescriptor)), Effect.map(Option.some), Effect.mapError((error) => error instanceof HarnessError ? error : new HarnessError({ code: "request.descriptor", message: error.message }))) : Effect.succeed(Option.none());
|
|
15
|
+
}, resolvingModel = (model, resolve) => {
|
|
16
|
+
return { ...model, [resolverKey]: resolve };
|
|
17
|
+
}, resolveModelRequest = (model) => {
|
|
18
|
+
const candidate = resolverKey in model ? model[resolverKey] : void 0;
|
|
19
|
+
return Effect.isEffect(candidate) ? candidate : Effect.succeed(model);
|
|
20
|
+
};
|
|
21
|
+
const initialHash = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225], rounds = [
|
|
22
|
+
1116352408,
|
|
23
|
+
1899447441,
|
|
24
|
+
3049323471,
|
|
25
|
+
3921009573,
|
|
26
|
+
961987163,
|
|
27
|
+
1508970993,
|
|
28
|
+
2453635748,
|
|
29
|
+
2870763221,
|
|
30
|
+
3624381080,
|
|
31
|
+
310598401,
|
|
32
|
+
607225278,
|
|
33
|
+
1426881987,
|
|
34
|
+
1925078388,
|
|
35
|
+
2162078206,
|
|
36
|
+
2614888103,
|
|
37
|
+
3248222580,
|
|
38
|
+
3835390401,
|
|
39
|
+
4022224774,
|
|
40
|
+
264347078,
|
|
41
|
+
604807628,
|
|
42
|
+
770255983,
|
|
43
|
+
1249150122,
|
|
44
|
+
1555081692,
|
|
45
|
+
1996064986,
|
|
46
|
+
2554220882,
|
|
47
|
+
2821834349,
|
|
48
|
+
2952996808,
|
|
49
|
+
3210313671,
|
|
50
|
+
3336571891,
|
|
51
|
+
3584528711,
|
|
52
|
+
113926993,
|
|
53
|
+
338241895,
|
|
54
|
+
666307205,
|
|
55
|
+
773529912,
|
|
56
|
+
1294757372,
|
|
57
|
+
1396182291,
|
|
58
|
+
1695183700,
|
|
59
|
+
1986661051,
|
|
60
|
+
2177026350,
|
|
61
|
+
2456956037,
|
|
62
|
+
2730485921,
|
|
63
|
+
2820302411,
|
|
64
|
+
3259730800,
|
|
65
|
+
3345764771,
|
|
66
|
+
3516065817,
|
|
67
|
+
3600352804,
|
|
68
|
+
4094571909,
|
|
69
|
+
275423344,
|
|
70
|
+
430227734,
|
|
71
|
+
506948616,
|
|
72
|
+
659060556,
|
|
73
|
+
883997877,
|
|
74
|
+
958139571,
|
|
75
|
+
1322822218,
|
|
76
|
+
1537002063,
|
|
77
|
+
1747873779,
|
|
78
|
+
1955562222,
|
|
79
|
+
2024104815,
|
|
80
|
+
2227730452,
|
|
81
|
+
2361852424,
|
|
82
|
+
2428436474,
|
|
83
|
+
2756734187,
|
|
84
|
+
3204031479,
|
|
85
|
+
3329325298
|
|
86
|
+
], rotr = (value, bits) => value >>> bits | value << 32 - bits, at = (words, index) => words[index] ?? 0, sha256Of = (text) => {
|
|
87
|
+
const bytes = new TextEncoder().encode(text), size = Math.ceil((bytes.length + 9) / 64) * 64, padded = new Uint8Array(size);
|
|
88
|
+
padded.set(bytes);
|
|
89
|
+
padded[bytes.length] = 128;
|
|
90
|
+
const view = new DataView(padded.buffer);
|
|
91
|
+
view.setUint32(size - 8, Math.floor(bytes.length / 536870912));
|
|
92
|
+
view.setUint32(size - 4, bytes.length * 8 >>> 0);
|
|
93
|
+
const schedule = (block) => Array.from({ length: 48 }, (_, index) => index + 16).reduce((words, index) => {
|
|
94
|
+
const early = at(words, index - 15), late = at(words, index - 2), s0 = rotr(early, 7) ^ rotr(early, 18) ^ early >>> 3, s1 = rotr(late, 17) ^ rotr(late, 19) ^ late >>> 10;
|
|
95
|
+
return [...words, at(words, index - 16) + s0 + at(words, index - 7) + s1 >>> 0];
|
|
96
|
+
}, Array.from({ length: 16 }, (_, index) => view.getUint32(block * 64 + index * 4))), compress = (hash, block) => {
|
|
97
|
+
const [a, b, c, d, e, f, g, h] = schedule(block).reduce(([a, b, c, d, e, f, g, h], word, index) => {
|
|
98
|
+
const t1 = h + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + at(rounds, index) + word >>> 0, t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
|
|
99
|
+
return [t1 + t2 >>> 0, a, b, c, d + t1 >>> 0, e, f, g];
|
|
100
|
+
}, hash);
|
|
101
|
+
return [
|
|
102
|
+
hash[0] + a >>> 0,
|
|
103
|
+
hash[1] + b >>> 0,
|
|
104
|
+
hash[2] + c >>> 0,
|
|
105
|
+
hash[3] + d >>> 0,
|
|
106
|
+
hash[4] + e >>> 0,
|
|
107
|
+
hash[5] + f >>> 0,
|
|
108
|
+
hash[6] + g >>> 0,
|
|
109
|
+
hash[7] + h >>> 0
|
|
110
|
+
];
|
|
111
|
+
};
|
|
112
|
+
return Array.from({ length: size / 64 }, (_, block) => block).reduce(compress, initialHash).map((word) => word.toString(16).padStart(8, "0")).join("");
|
|
113
|
+
}, providerArgs = (args) => ({
|
|
114
|
+
argKeys: typeof args === "object" && args !== null && !Array.isArray(args) ? Object.keys(args).sort() : [],
|
|
115
|
+
argsDigest: sha256Of(canonicalJson(args))
|
|
116
|
+
});
|
|
117
|
+
export const modelRequestTools = (tools) => tools.map((tool) => ({
|
|
118
|
+
name: tool.name,
|
|
119
|
+
description: Tool.getDescription(tool) ?? "",
|
|
120
|
+
parameters: Tool.isDynamic(tool) && tool.jsonSchema !== void 0 ? tool.jsonSchema : toolParametersSchema(tool),
|
|
121
|
+
provider: Tool.isProviderDefined(tool) ? Option.some({ id: tool.id, name: tool.providerName, ...providerArgs(tool.args) }) : Option.none()
|
|
122
|
+
})), modelRequestHeaderOf = (events, runId, step) => {
|
|
123
|
+
const header = events.filter((event) => event.kind === "request.prepared" && event.data.runId === runId && event.data.step === step).at(-1);
|
|
124
|
+
return header === void 0 ? Effect.fail(new HarnessError({ code: "request.missing", message: `No durable request header for ${runId} step ${step}` })) : Schema.decodeUnknownEffect(ModelRequestHeader)(header.data).pipe(Effect.mapError((error) => new HarnessError({ code: "request.decode", message: error.message })));
|
|
125
|
+
};
|
|
126
|
+
const diverged = (part, detail) => new HarnessError({ code: "request.diverged", message: `${part}: ${detail}` }), entriesThrough = (entries, through) => Option.match(through, {
|
|
127
|
+
onNone: () => Effect.succeed([]),
|
|
128
|
+
onSome: (id) => {
|
|
129
|
+
const cut = entries.findIndex((entry) => entry.id === id);
|
|
130
|
+
return cut < 0 ? Effect.fail(diverged("context", `the memory entry ${id} the request was built through is not in the events`)) : Effect.succeed(entries.slice(0, cut + 1));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
export const reconstructModelRequest = (header, entries) => Effect.gen(function* () {
|
|
134
|
+
const context = buildContext(yield* entriesThrough(entries, header.through), header.render);
|
|
135
|
+
if (context.fingerprint !== header.contextFingerprint)
|
|
136
|
+
return yield* Effect.fail(diverged("context", `the memory events rebuild context ${context.fingerprint}, the request was prepared from ${header.contextFingerprint}`));
|
|
137
|
+
return {
|
|
138
|
+
prompt: Prompt.concat(Prompt.make([{ role: "system", content: header.system }]), Prompt.make(toPromptMessages(context.messages))),
|
|
139
|
+
contextFingerprint: context.fingerprint,
|
|
140
|
+
tools: header.tools,
|
|
141
|
+
toolChoice: header.toolChoice,
|
|
142
|
+
model: header.model,
|
|
143
|
+
cacheKey: header.cacheKey,
|
|
144
|
+
callPolicy: header.callPolicy
|
|
145
|
+
};
|
|
146
|
+
}), replayModelRequest = (events, runId, step) => Effect.gen(function* () {
|
|
147
|
+
const header = yield* modelRequestHeaderOf(events, runId, step), position = events.findLastIndex((event) => event.kind === "request.prepared" && event.data.runId === runId && event.data.step === step), entries = yield* entriesOfEvents(events.slice(0, position + 1)).pipe(Effect.mapError((error) => new HarnessError({ code: "request.memory", message: error.message })));
|
|
148
|
+
return yield* reconstructModelRequest(header, entries);
|
|
149
|
+
}), freezeModelRequest = (value) => {
|
|
150
|
+
if (typeof value !== "object" || value === null)
|
|
151
|
+
return value;
|
|
152
|
+
Object.values(value).map(freezeModelRequest);
|
|
153
|
+
return Object.freeze(value);
|
|
154
|
+
};
|
|
155
|
+
const requestParts = (request) => [
|
|
156
|
+
["system", request.prompt.content.slice(0, 1)],
|
|
157
|
+
["messages", request.prompt.content.slice(1)],
|
|
158
|
+
["tools", request.tools],
|
|
159
|
+
["toolChoice", request.toolChoice],
|
|
160
|
+
["model", request.model],
|
|
161
|
+
["cacheKey", request.cacheKey],
|
|
162
|
+
["callPolicy", request.callPolicy]
|
|
163
|
+
];
|
|
164
|
+
export const checkModelRequest = (expected, actual) => {
|
|
165
|
+
const dispatched = requestParts(actual), differing = requestParts(expected).find(([, value], index) => canonicalJson(value) !== canonicalJson(dispatched[index]?.[1]));
|
|
166
|
+
return differing === void 0 ? Effect.void : Effect.fail(diverged(differing[0], "the dispatched request differs from its saved header and memory events"));
|
|
167
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { ModelCallPolicy } from "../domain/model-call-policy.entity.js";
|
|
3
|
+
import { EntryId } from "../memory/memory-log.entity.js";
|
|
4
|
+
export const ModelRequestDescriptor = Schema.Struct({
|
|
5
|
+
provider: Schema.String,
|
|
6
|
+
model: Schema.String,
|
|
7
|
+
settings: Schema.Record(Schema.String, Schema.Unknown)
|
|
8
|
+
}), ModelRequestTool = Schema.Struct({
|
|
9
|
+
name: Schema.String,
|
|
10
|
+
description: Schema.String,
|
|
11
|
+
parameters: Schema.Unknown,
|
|
12
|
+
provider: Schema.OptionFromNullOr(Schema.Struct({
|
|
13
|
+
id: Schema.String,
|
|
14
|
+
name: Schema.String,
|
|
15
|
+
argKeys: Schema.Array(Schema.String),
|
|
16
|
+
argsDigest: Schema.String
|
|
17
|
+
}))
|
|
18
|
+
}), MemoryRenderRecipe = Schema.Struct({
|
|
19
|
+
strategy: Schema.String,
|
|
20
|
+
currentTurn: Schema.Int,
|
|
21
|
+
currentRun: Schema.String,
|
|
22
|
+
turnContext: Schema.Literals(["current", "all"]),
|
|
23
|
+
replies: Schema.Boolean,
|
|
24
|
+
stepContext: Schema.Literals(["tail", "none"]),
|
|
25
|
+
digests: Schema.Boolean,
|
|
26
|
+
media: Schema.Struct({ mode: Schema.Literals(["none", "inline"]), maxImages: Schema.Int })
|
|
27
|
+
}), ModelRequestHeader = Schema.Struct({
|
|
28
|
+
version: Schema.Literal(1),
|
|
29
|
+
runId: Schema.String,
|
|
30
|
+
step: Schema.Int,
|
|
31
|
+
strategyVersion: Schema.String,
|
|
32
|
+
system: Schema.String,
|
|
33
|
+
render: MemoryRenderRecipe,
|
|
34
|
+
through: Schema.OptionFromNullOr(EntryId),
|
|
35
|
+
contextFingerprint: Schema.String,
|
|
36
|
+
tools: Schema.Array(ModelRequestTool),
|
|
37
|
+
toolChoice: Schema.Unknown,
|
|
38
|
+
model: Schema.OptionFromNullOr(ModelRequestDescriptor),
|
|
39
|
+
cacheKey: Schema.OptionFromNullOr(Schema.String),
|
|
40
|
+
callPolicy: Schema.OptionFromNullOr(ModelCallPolicy)
|
|
41
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { HarnessError } from "../harness/plugin.entity.js";
|
|
3
|
+
import type { PromptContext, PromptSection } from "../ports/capability.port.js";
|
|
4
|
+
/** Static sections first (the cacheable prefix), then session ones; each tier by order, then id. */
|
|
5
|
+
export declare const orderSections: (sections: ReadonlyArray<PromptSection>) => ReadonlyArray<PromptSection>;
|
|
6
|
+
/** Render sections in order; a section that renders None is left out. Their requirements are the caller's. */
|
|
7
|
+
export declare const renderSections: (sections: ReadonlyArray<PromptSection>, context: PromptContext) => Effect.Effect<ReadonlyArray<{
|
|
8
|
+
readonly section: PromptSection;
|
|
9
|
+
readonly text: string;
|
|
10
|
+
}>, HarnessError, unknown>;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
const tierRank = (section) => section.tier === "static" ? 0 : section.tier === "session" ? 1 : 2;
|
|
3
|
+
export const orderSections = (sections) => [...sections].sort((left, right) => tierRank(left) - tierRank(right) || left.order - right.order || left.id.localeCompare(right.id)), renderSections = (sections, context) => Effect.forEach(orderSections(sections), (section) => section.render(context).pipe(Effect.map((text) => Option.map(text, (value) => ({ section, text: value }))))).pipe(Effect.map((rendered) => rendered.flatMap(Option.toArray)));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Response } from "effect/ai";
|
|
2
|
+
import { Context } from "effect";
|
|
3
|
+
import type { ConformanceCheck } from "../conformance.entity.js";
|
|
4
|
+
import type { StepLoop } from "../ports/step-loop.port.js";
|
|
5
|
+
type Part = Response.PartEncoded;
|
|
6
|
+
/** A scripted provider reply calling `probe`. */
|
|
7
|
+
export declare const probeCall: (id: string, n: number) => ReadonlyArray<Part>;
|
|
8
|
+
export declare const textReply: (text: string) => ReadonlyArray<Part>;
|
|
9
|
+
/**
|
|
10
|
+
* The `StepLoop` contract, as checks any implementation must pass:
|
|
11
|
+
* per-step event order, recorded results on `step.ended`, planned batches
|
|
12
|
+
* without a provider call, forced tool choices, awaiting verdicts, no call
|
|
13
|
+
* after completion, and the step cap.
|
|
14
|
+
*/
|
|
15
|
+
export declare const stepLoopConformance: (loop: Context.Service.Shape<typeof StepLoop>) => ReadonlyArray<ConformanceCheck>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { LanguageModel, Tool, Toolkit } from "effect/ai";
|
|
2
|
+
import { Context, Effect, Option, Ref, Schema, Stream } from "effect";
|
|
3
|
+
import { ConformanceFailure } from "../conformance.entity.js";
|
|
4
|
+
import { Failure } from "../domain/failure.entity.js";
|
|
5
|
+
import { HarnessError } from "../harness/plugin.entity.js";
|
|
6
|
+
import { CurrentAgentStep } from "../loop/stepContext.js";
|
|
7
|
+
import { entryId } from "../memory/memory-log.entity.functions.js";
|
|
8
|
+
import { makeTurnEvents, makeTurnTasks } from "./turn-bus.js";
|
|
9
|
+
const Probe = Tool.make("probe", {
|
|
10
|
+
description: "Echo a number.",
|
|
11
|
+
parameters: Schema.Struct({ n: Schema.Finite }),
|
|
12
|
+
success: Schema.Number,
|
|
13
|
+
failure: Failure,
|
|
14
|
+
failureMode: "return"
|
|
15
|
+
}), typed = Toolkit.make(Probe), erased = [Probe], toolkit = Toolkit.make(...erased), usage = { inputTokens: { total: 10 }, outputTokens: { total: 2 } };
|
|
16
|
+
export const probeCall = (id, n) => [
|
|
17
|
+
{ type: "tool-call", id, name: Probe.name, params: { n }, providerExecuted: !1 },
|
|
18
|
+
{ type: "finish", reason: "tool-calls", usage }
|
|
19
|
+
], textReply = (text) => [{ type: "text", text }, { type: "finish", reason: "stop", usage }];
|
|
20
|
+
const incomplete = { complete: !1, awaiting: [], facts: {} }, observe = (loop, scenario) => Effect.scoped(Effect.gen(function* () {
|
|
21
|
+
const scope = yield* Effect.scope, events = yield* makeTurnEvents({ maxDepth: 8 }), tasks = yield* makeTurnTasks(scope), seen = yield* Ref.make([]);
|
|
22
|
+
yield* events.subscribe(Option.some, (event) => Ref.update(seen, (all) => [...all, event]));
|
|
23
|
+
const calls = yield* Ref.make([]), dispatches = yield* Ref.make([]), model = yield* LanguageModel.make({
|
|
24
|
+
generateText: (options) => Ref.modify(calls, (all) => [all.length, [...all, options.toolChoice]]).pipe(Effect.tap(() => scenario.providerCall ?? Effect.void), Effect.map((index) => [...scenario.script[index] ?? textReply("done")])),
|
|
25
|
+
streamText: () => Stream.die("the conformance provider does not stream")
|
|
26
|
+
}), invocations = yield* Ref.make(0), handlers = yield* typed.toHandlers({
|
|
27
|
+
probe: ({ n }) => Effect.gen(function* () {
|
|
28
|
+
const step = Option.getOrElse(yield* Effect.service(CurrentAgentStep), () => -1), invocationId = `probe:${yield* Ref.getAndUpdate(invocations, (value) => value + 1)}`, base = { step, invocationId, tool: Probe.name, input: { n }, labels: {}, stage: Option.none() };
|
|
29
|
+
yield* events.publish({ _tag: "tool.started", ...base }).pipe(Effect.orDie);
|
|
30
|
+
yield* events.publish({ _tag: "tool.completed", ...base, ok: !0, result: n, encoded: n, durationMs: 0 }).pipe(Effect.orDie);
|
|
31
|
+
return n;
|
|
32
|
+
})
|
|
33
|
+
}), tools = {
|
|
34
|
+
toolkit,
|
|
35
|
+
handlers,
|
|
36
|
+
active: Effect.succeed([Probe.name]),
|
|
37
|
+
activate: () => Effect.succeed([Probe.name]),
|
|
38
|
+
match: (userMessage) => Effect.succeed({ userMessage, skills: [], probabilities: Option.none(), record: Option.none() }),
|
|
39
|
+
apply: () => Effect.succeed([Probe.name]),
|
|
40
|
+
select: () => Effect.succeed([Probe.name]),
|
|
41
|
+
views: {
|
|
42
|
+
view: (_tool, encoded) => Effect.succeed({ text: String(encoded), version: "1", subjects: [], artifacts: [], pinned: !1 }),
|
|
43
|
+
compact: () => Effect.succeed(Option.none()),
|
|
44
|
+
digest: () => Effect.succeed(Option.none())
|
|
45
|
+
},
|
|
46
|
+
pollable: [],
|
|
47
|
+
skills: []
|
|
48
|
+
}, recorded = yield* Ref.make([]), record = (step, tail) => Ref.modify(recorded, (all) => {
|
|
49
|
+
const entries = tail.flatMap((message) => message.role !== "tool" ? [{ _tag: "Message", message }] : message.content.map((part) => ({
|
|
50
|
+
_tag: "ToolResult",
|
|
51
|
+
toolCallId: part.toolCallId,
|
|
52
|
+
toolName: part.toolName,
|
|
53
|
+
isError: part.isError ?? !1,
|
|
54
|
+
encoded: part.output,
|
|
55
|
+
view: String(part.output),
|
|
56
|
+
viewVersion: "1",
|
|
57
|
+
subjects: [],
|
|
58
|
+
artifacts: [],
|
|
59
|
+
pinned: !1
|
|
60
|
+
}))).map((body, index) => ({ id: entryId("conformance", all.length + index), runId: "conformance", turn: 1, step, at: 0, body }));
|
|
61
|
+
return [entries, [...all, ...entries]];
|
|
62
|
+
});
|
|
63
|
+
yield* scenario.before?.(tasks) ?? Effect.void;
|
|
64
|
+
const request = {
|
|
65
|
+
tools,
|
|
66
|
+
handlers: Context.merge(handlers, Context.make(LanguageModel.LanguageModel, model)),
|
|
67
|
+
limits: { maxSteps: scenario.maxSteps ?? 6, toolConcurrency: 1, streaming: !1, requireCompletion: !1 },
|
|
68
|
+
initial: Option.fromNullishOr(scenario.initial),
|
|
69
|
+
plan: () => Effect.succeed({ model: Option.none(), system: "conformance", messages: [{ role: "user", content: "go" }], toolChoice: Option.fromNullishOr(scenario.toolChoice) }),
|
|
70
|
+
dispatch: (step) => Ref.update(dispatches, (all) => [...all, step]).pipe(Effect.andThen(scenario.dispatch?.(step) ?? Effect.void)),
|
|
71
|
+
record,
|
|
72
|
+
completion: (info) => scenario.completion?.(info.stepIndex, tasks) ?? Effect.succeed(incomplete),
|
|
73
|
+
steering: Effect.succeed(Option.none()),
|
|
74
|
+
correctives: Option.none(),
|
|
75
|
+
events,
|
|
76
|
+
tasks,
|
|
77
|
+
cacheKey: Option.none()
|
|
78
|
+
};
|
|
79
|
+
return { result: yield* loop.run(request), events: yield* Ref.get(seen), providerCalls: yield* Ref.get(calls), recorded: yield* Ref.get(recorded), dispatches: yield* Ref.get(dispatches) };
|
|
80
|
+
}).pipe(Effect.mapError((error) => new ConformanceFailure({ check: "run", message: error.message })))), expect = (check, holds, message) => holds ? Effect.void : Effect.fail(new ConformanceFailure({ check, message })), stepOf = (event) => ("step" in event) ? Option.some(event.step) : Option.none(), indexOf = (events, tag, step) => events.findIndex((event) => event._tag === tag && Option.contains(stepOf(event), step));
|
|
81
|
+
export const stepLoopConformance = (loop) => [
|
|
82
|
+
{
|
|
83
|
+
name: "validates every real request immediately before dispatch, excluding planned batches",
|
|
84
|
+
run: Effect.gen(function* () {
|
|
85
|
+
const { dispatches, providerCalls } = yield* observe(loop, { initial: { calls: [{ name: Probe.name, params: { n: 2 } }], skills: [] }, script: [textReply("done")] });
|
|
86
|
+
yield* expect("dispatch", dispatches.join() === "1" && providerCalls.length === 1, `dispatches ${dispatches.join()}, ${providerCalls.length} provider calls`);
|
|
87
|
+
})
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "a failed durable request check prevents the provider call",
|
|
91
|
+
run: Effect.gen(function* () {
|
|
92
|
+
const calls = yield* Ref.make(0), result = yield* Effect.result(observe(loop, {
|
|
93
|
+
script: [textReply("must not run")],
|
|
94
|
+
providerCall: Ref.update(calls, (count) => count + 1),
|
|
95
|
+
dispatch: () => Effect.fail(new HarnessError({ code: "request.diverged", message: "fixture request changed" }))
|
|
96
|
+
}));
|
|
97
|
+
yield* expect("dispatch", result._tag === "Failure" && (yield* Ref.get(calls)) === 0, "a rejected request reached the provider");
|
|
98
|
+
})
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "orders step.started < tool.* < step.ended < completion.evaluated within every step",
|
|
102
|
+
run: Effect.gen(function* () {
|
|
103
|
+
const check = "order", { events } = yield* observe(loop, { script: [probeCall("c1", 1), textReply("done")] });
|
|
104
|
+
yield* Effect.forEach([0, 1], (step) => Effect.gen(function* () {
|
|
105
|
+
const started = indexOf(events, "step.started", step), ended = indexOf(events, "step.ended", step), evaluated = indexOf(events, "completion.evaluated", step), toolEvents = events.flatMap((event, index) => (event._tag === "tool.started" || event._tag === "tool.completed") && event.step === step ? [index] : []);
|
|
106
|
+
yield* expect(check, started >= 0 && ended > started && evaluated > ended, `step ${step}: started ${started}, ended ${ended}, evaluated ${evaluated}`);
|
|
107
|
+
yield* expect(check, toolEvents.every((index) => index > started && index < ended), `step ${step}: tool events outside the step`);
|
|
108
|
+
}));
|
|
109
|
+
yield* expect(check, events.some((event) => event._tag === "tool.completed" && event.step === 0), "the probe call of step 0 was not published");
|
|
110
|
+
})
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "step.ended lists the step's recorded tool results by entry id",
|
|
114
|
+
run: Effect.gen(function* () {
|
|
115
|
+
const { events, recorded } = yield* observe(loop, { script: [probeCall("c1", 1), textReply("done")] }), ended = events.find((event) => event._tag === "step.ended" && event.step === 0), results = ended?._tag === "step.ended" ? ended.results : [], expected = recorded.filter((entry) => entry.step === 0 && entry.body._tag === "ToolResult").map((entry) => String(entry.id));
|
|
116
|
+
yield* expect("results", results.length === 1 && results.map((result) => String(result.entry)).join() === expected.join() && results[0]?.tool === Probe.name, `expected ${expected.join()}, got ${results.map((result) => result.entry).join()}`);
|
|
117
|
+
})
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "a planned first batch runs without a provider call",
|
|
121
|
+
run: Effect.gen(function* () {
|
|
122
|
+
const { events, providerCalls } = yield* observe(loop, { initial: { calls: [{ name: Probe.name, params: { n: 2 } }], skills: [] }, script: [textReply("done")] }), first = events.find((event) => event._tag === "step.started" && event.step === 0);
|
|
123
|
+
yield* expect("planned", first?._tag === "step.started" && first.planned, "step 0 is not marked planned");
|
|
124
|
+
yield* expect("planned", events.some((event) => event._tag === "tool.completed" && event.step === 0), "the planned call did not run in step 0");
|
|
125
|
+
yield* expect("planned", providerCalls.length === 1, `expected 1 provider call, got ${providerCalls.length}`);
|
|
126
|
+
})
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "a forced tool choice reaches the provider",
|
|
130
|
+
run: Effect.gen(function* () {
|
|
131
|
+
const { providerCalls } = yield* observe(loop, { toolChoice: { tool: Probe.name }, script: [textReply("done")] }), choice = providerCalls[0];
|
|
132
|
+
yield* expect("tool-choice", typeof choice === "object" && choice !== null && "tool" in choice && choice.tool === Probe.name, `the provider saw ${JSON.stringify(choice)}`);
|
|
133
|
+
})
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "an awaiting verdict joins its tasks and is evaluated once more",
|
|
137
|
+
run: Effect.gen(function* () {
|
|
138
|
+
const done = yield* Ref.make(!1), { events, providerCalls, result } = yield* observe(loop, {
|
|
139
|
+
script: [probeCall("c1", 1)],
|
|
140
|
+
before: (tasks) => tasks.fork("work", Effect.sleep("20 millis").pipe(Effect.andThen(Ref.set(done, !0)))),
|
|
141
|
+
completion: () => Ref.get(done).pipe(Effect.map((complete) => ({ complete, awaiting: complete ? [] : ["work"], facts: {} })))
|
|
142
|
+
}), verdicts = events.flatMap((event) => event._tag === "completion.evaluated" && event.step === 0 ? [event.verdict] : []);
|
|
143
|
+
yield* expect("awaiting", verdicts.length === 2 && verdicts[0]?.complete === !1 && verdicts[1]?.complete === !0, `step 0 verdicts: ${JSON.stringify(verdicts)}`);
|
|
144
|
+
yield* expect("awaiting", providerCalls.length === 1 && result.outcome === "completed", `${providerCalls.length} provider calls, ${result.outcome}`);
|
|
145
|
+
})
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "no provider call follows a complete verdict",
|
|
149
|
+
run: Effect.gen(function* () {
|
|
150
|
+
const { providerCalls, result } = yield* observe(loop, {
|
|
151
|
+
script: [probeCall("c1", 1), probeCall("c2", 2)],
|
|
152
|
+
completion: () => Effect.succeed({ complete: !0, awaiting: [], facts: {} })
|
|
153
|
+
});
|
|
154
|
+
yield* expect("complete", providerCalls.length === 1 && result.outcome === "completed", `${providerCalls.length} provider calls, ${result.outcome}`);
|
|
155
|
+
})
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "the step cap ends the run as partial",
|
|
159
|
+
run: Effect.gen(function* () {
|
|
160
|
+
const { providerCalls, result } = yield* observe(loop, { maxSteps: 2, script: [probeCall("c1", 1), probeCall("c2", 2), probeCall("c3", 3)] });
|
|
161
|
+
yield* expect("step-cap", result.outcome === "partial" && result.reason === "step-cap" && providerCalls.length === 2, `${result.outcome}/${result.reason} after ${providerCalls.length} provider calls`);
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Effect, Option, Ref } from "effect";
|
|
2
|
+
import { ConversationId } from "../domain/message.entity.js";
|
|
3
|
+
import type { TurnWriter } from "../ports/sessions.port.js";
|
|
4
|
+
import type { SessionLogEvent } from "../session/session-log.entity.js";
|
|
5
|
+
/**
|
|
6
|
+
* A turn writer that keeps the session's events in memory, for testing the
|
|
7
|
+
* turn's pieces without a sessions plugin: the turn is begun (its
|
|
8
|
+
* `turn.started` stored) over `history`, writes are stored at once, and the
|
|
9
|
+
* turn is never closed from outside.
|
|
10
|
+
*/
|
|
11
|
+
export declare const recordingTurnWriter: (input: {
|
|
12
|
+
readonly runId: string;
|
|
13
|
+
readonly text: string;
|
|
14
|
+
readonly turn?: number;
|
|
15
|
+
readonly history?: ReadonlyArray<SessionLogEvent>;
|
|
16
|
+
readonly conversation?: ConversationId;
|
|
17
|
+
}) => Effect.Effect<{
|
|
18
|
+
writer: TurnWriter;
|
|
19
|
+
stored: Ref.Ref<readonly {
|
|
20
|
+
readonly session: string & import("effect/Brand").Brand<"ConversationId">;
|
|
21
|
+
readonly seq: number;
|
|
22
|
+
readonly turn: Option.Option<number>;
|
|
23
|
+
readonly kind: string;
|
|
24
|
+
readonly at: number;
|
|
25
|
+
readonly data: {
|
|
26
|
+
readonly [x: string]: unknown;
|
|
27
|
+
};
|
|
28
|
+
}[]>;
|
|
29
|
+
kinds: Effect.Effect<string[], never, never>;
|
|
30
|
+
}, never, never>;
|