@namzu/sdk 62.12.0 → 62.14.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/CHANGELOG.md +31 -0
- package/dist/agents/file-definitions.d.ts +7 -0
- package/dist/agents/file-definitions.d.ts.map +1 -1
- package/dist/agents/file-definitions.js +20 -0
- package/dist/agents/file-definitions.js.map +1 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +1 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/bridge/acp/server.d.ts +8 -1
- package/dist/bridge/acp/server.d.ts.map +1 -1
- package/dist/bridge/acp/server.js +12 -0
- package/dist/bridge/acp/server.js.map +1 -1
- package/dist/connector/index.d.ts +1 -1
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/client.d.ts +96 -0
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +488 -13
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/index.d.ts +1 -0
- package/dist/connector/mcp/index.d.ts.map +1 -1
- package/dist/connector/mcp/index.js +1 -0
- package/dist/connector/mcp/index.js.map +1 -1
- package/dist/connector/mcp/tasks.d.ts +40 -0
- package/dist/connector/mcp/tasks.d.ts.map +1 -0
- package/dist/connector/mcp/tasks.js +55 -0
- package/dist/connector/mcp/tasks.js.map +1 -0
- package/dist/constants/data-epoch.d.ts +18 -0
- package/dist/constants/data-epoch.d.ts.map +1 -0
- package/dist/constants/data-epoch.js +18 -0
- package/dist/constants/data-epoch.js.map +1 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +1 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/constants/mcp/index.d.ts +1 -1
- package/dist/constants/mcp/index.d.ts.map +1 -1
- package/dist/constants/mcp/index.js +5 -0
- package/dist/constants/mcp/index.js.map +1 -1
- package/dist/constants/telemetry/index.d.ts +12 -0
- package/dist/constants/telemetry/index.d.ts.map +1 -1
- package/dist/constants/telemetry/index.js +12 -0
- package/dist/constants/telemetry/index.js.map +1 -1
- package/dist/eval/bootstrap.d.ts +122 -0
- package/dist/eval/bootstrap.d.ts.map +1 -0
- package/dist/eval/bootstrap.js +227 -0
- package/dist/eval/bootstrap.js.map +1 -0
- package/dist/eval/compare.d.ts +109 -0
- package/dist/eval/compare.d.ts.map +1 -0
- package/dist/eval/compare.js +324 -0
- package/dist/eval/compare.js.map +1 -0
- package/dist/eval/disclosure.d.ts +70 -0
- package/dist/eval/disclosure.d.ts.map +1 -0
- package/dist/eval/disclosure.js +64 -0
- package/dist/eval/disclosure.js.map +1 -0
- package/dist/eval/experiment.d.ts +23 -0
- package/dist/eval/experiment.d.ts.map +1 -1
- package/dist/eval/experiment.js +87 -15
- package/dist/eval/experiment.js.map +1 -1
- package/dist/eval/from-turn.d.ts.map +1 -1
- package/dist/eval/from-turn.js +5 -0
- package/dist/eval/from-turn.js.map +1 -1
- package/dist/eval/index.d.ts +13 -2
- package/dist/eval/index.d.ts.map +1 -1
- package/dist/eval/index.js +6 -1
- package/dist/eval/index.js.map +1 -1
- package/dist/eval/report-stats.d.ts +77 -0
- package/dist/eval/report-stats.d.ts.map +1 -0
- package/dist/eval/report-stats.js +184 -0
- package/dist/eval/report-stats.js.map +1 -0
- package/dist/eval/run-options.d.ts +30 -0
- package/dist/eval/run-options.d.ts.map +1 -0
- package/dist/eval/run-options.js +20 -0
- package/dist/eval/run-options.js.map +1 -0
- package/dist/eval/run-summary.d.ts +36 -0
- package/dist/eval/run-summary.d.ts.map +1 -0
- package/dist/eval/run-summary.js +57 -0
- package/dist/eval/run-summary.js.map +1 -0
- package/dist/eval/scorers.d.ts +32 -0
- package/dist/eval/scorers.d.ts.map +1 -1
- package/dist/eval/scorers.js +77 -0
- package/dist/eval/scorers.js.map +1 -1
- package/dist/eval/types.d.ts +74 -0
- package/dist/eval/types.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts +3 -0
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +18 -2
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1 -1
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/loader.d.ts +11 -0
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +15 -2
- package/dist/plugin/loader.js.map +1 -1
- package/dist/public-runtime.d.ts +10 -6
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +13 -5
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.d.ts +3 -0
- package/dist/registry/command/kernel-commands.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.js +76 -5
- package/dist/registry/command/kernel-commands.js.map +1 -1
- package/dist/runtime/query/cancelled-before-start.d.ts.map +1 -1
- package/dist/runtime/query/cancelled-before-start.js +2 -0
- package/dist/runtime/query/cancelled-before-start.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +17 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +18 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +80 -1
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +18 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +5 -0
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/result.d.ts.map +1 -1
- package/dist/runtime/query/result.js +2 -0
- package/dist/runtime/query/result.js.map +1 -1
- package/dist/runtime/query/turn/answer-claims.d.ts +52 -0
- package/dist/runtime/query/turn/answer-claims.d.ts.map +1 -0
- package/dist/runtime/query/turn/answer-claims.js +348 -0
- package/dist/runtime/query/turn/answer-claims.js.map +1 -0
- package/dist/runtime/query/turn/outcome-ledger.d.ts +7 -0
- package/dist/runtime/query/turn/outcome-ledger.d.ts.map +1 -1
- package/dist/runtime/query/turn/outcome-ledger.js +13 -0
- package/dist/runtime/query/turn/outcome-ledger.js.map +1 -1
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/loader.d.ts +15 -1
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +17 -3
- package/dist/skills/loader.js.map +1 -1
- package/dist/store/goal/index.d.ts +3 -1
- package/dist/store/goal/index.d.ts.map +1 -1
- package/dist/store/goal/index.js +6 -0
- package/dist/store/goal/index.js.map +1 -1
- package/dist/telemetry/genai.d.ts +98 -0
- package/dist/telemetry/genai.d.ts.map +1 -0
- package/dist/telemetry/genai.js +189 -0
- package/dist/telemetry/genai.js.map +1 -0
- package/dist/telemetry/runtime-accessors.d.ts +19 -0
- package/dist/telemetry/runtime-accessors.d.ts.map +1 -1
- package/dist/telemetry/runtime-accessors.js +43 -1
- package/dist/telemetry/runtime-accessors.js.map +1 -1
- package/dist/tools/goal/confirmation.d.ts +154 -0
- package/dist/tools/goal/confirmation.d.ts.map +1 -0
- package/dist/tools/goal/confirmation.js +290 -0
- package/dist/tools/goal/confirmation.js.map +1 -0
- package/dist/tools/goal/index.d.ts +4 -1
- package/dist/tools/goal/index.d.ts.map +1 -1
- package/dist/tools/goal/index.js +27 -2
- package/dist/tools/goal/index.js.map +1 -1
- package/dist/tools/hand-back/index.d.ts +48 -0
- package/dist/tools/hand-back/index.d.ts.map +1 -0
- package/dist/tools/hand-back/index.js +129 -0
- package/dist/tools/hand-back/index.js.map +1 -0
- package/dist/tools/schedules/schedule-tool.d.ts.map +1 -1
- package/dist/tools/schedules/schedule-tool.js +15 -1
- package/dist/tools/schedules/schedule-tool.js.map +1 -1
- package/dist/tools/schedules/types.d.ts +10 -0
- package/dist/tools/schedules/types.d.ts.map +1 -1
- package/dist/toolsets/manager.d.ts.map +1 -1
- package/dist/toolsets/manager.js +12 -0
- package/dist/toolsets/manager.js.map +1 -1
- package/dist/types/acp/index.d.ts +18 -0
- package/dist/types/acp/index.d.ts.map +1 -1
- package/dist/types/connector/mcp.d.ts +18 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/goal/index.d.ts +12 -0
- package/dist/types/goal/index.d.ts.map +1 -1
- package/dist/types/session/config.d.ts +8 -0
- package/dist/types/session/config.d.ts.map +1 -1
- package/dist/types/session/hand-back.d.ts +29 -0
- package/dist/types/session/hand-back.d.ts.map +1 -0
- package/dist/types/session/hand-back.js +11 -0
- package/dist/types/session/hand-back.js.map +1 -0
- package/dist/types/session/outcome.d.ts +37 -0
- package/dist/types/session/outcome.d.ts.map +1 -1
- package/dist/types/session/outcome.js.map +1 -1
- package/dist/types/session/turn.d.ts +19 -1
- package/dist/types/session/turn.d.ts.map +1 -1
- package/dist/types/session/turn.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/file-definitions.ts +21 -0
- package/src/agents/index.ts +1 -0
- package/src/bridge/acp/server.ts +20 -0
- package/src/connector/index.ts +1 -0
- package/src/connector/mcp/client.ts +531 -14
- package/src/connector/mcp/index.ts +1 -0
- package/src/connector/mcp/tasks.ts +81 -0
- package/src/constants/data-epoch.ts +17 -0
- package/src/constants/index.ts +1 -0
- package/src/constants/mcp/index.ts +6 -1
- package/src/constants/telemetry/index.ts +13 -0
- package/src/eval/bootstrap.ts +314 -0
- package/src/eval/compare.ts +473 -0
- package/src/eval/disclosure.ts +136 -0
- package/src/eval/experiment.ts +127 -15
- package/src/eval/from-turn.ts +5 -0
- package/src/eval/index.ts +51 -0
- package/src/eval/report-stats.ts +257 -0
- package/src/eval/run-options.ts +49 -0
- package/src/eval/run-summary.ts +104 -0
- package/src/eval/scorers.ts +83 -0
- package/src/eval/types.ts +76 -0
- package/src/manager/session/turn-recorder.ts +23 -2
- package/src/plugin/index.ts +7 -1
- package/src/plugin/loader.ts +16 -2
- package/src/public-runtime.ts +70 -0
- package/src/public-types.ts +9 -0
- package/src/registry/command/kernel-commands.ts +94 -6
- package/src/runtime/query/cancelled-before-start.ts +2 -0
- package/src/runtime/query/index.ts +20 -0
- package/src/runtime/query/iteration/index.ts +85 -1
- package/src/runtime/query/iteration/phases/tool-review.ts +24 -1
- package/src/runtime/query/iteration/stream-turn.ts +10 -0
- package/src/runtime/query/result.ts +2 -0
- package/src/runtime/query/turn/answer-claims.ts +413 -0
- package/src/runtime/query/turn/outcome-ledger.ts +12 -0
- package/src/skills/index.ts +1 -0
- package/src/skills/loader.ts +21 -3
- package/src/store/goal/index.ts +11 -0
- package/src/telemetry/genai.ts +202 -0
- package/src/telemetry/runtime-accessors.ts +40 -1
- package/src/tools/goal/confirmation.ts +375 -0
- package/src/tools/goal/index.ts +37 -1
- package/src/tools/hand-back/index.ts +150 -0
- package/src/tools/schedules/schedule-tool.ts +19 -1
- package/src/tools/schedules/types.ts +10 -0
- package/src/toolsets/manager.ts +22 -0
- package/src/types/acp/index.ts +20 -0
- package/src/types/connector/mcp.ts +18 -0
- package/src/types/goal/index.ts +13 -0
- package/src/types/session/config.ts +8 -0
- package/src/types/session/hand-back.ts +31 -0
- package/src/types/session/outcome.ts +42 -0
- package/src/types/session/turn.ts +19 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { Attributes } from '@opentelemetry/api'
|
|
2
|
+
import { isProviderRequestError } from '../provider/errors.js'
|
|
3
|
+
import type { TokenUsage } from '../types/common/index.js'
|
|
4
|
+
import { GENAI } from './attributes.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The OpenTelemetry semantic conventions for generative AI, as far as the
|
|
8
|
+
* spans of this kernel follow them.
|
|
9
|
+
*
|
|
10
|
+
* The convention is still marked "Development", so its names can change
|
|
11
|
+
* between releases. What the kernel emits is therefore pinned to ONE released
|
|
12
|
+
* version, and says so: the instrumentation scope carries the schema URL of
|
|
13
|
+
* that version. The text of the convention was published in the
|
|
14
|
+
* `semantic-conventions` repository up to and including v1.41.1; later
|
|
15
|
+
* releases only point to a new repository. Moving the pin means reading the
|
|
16
|
+
* new text, changing {@link GENAI_SEMCONV_VERSION} and the table in
|
|
17
|
+
* `docs/sdk/telemetry-genai.md` together; a test fails when an attribute name
|
|
18
|
+
* is emitted that is not in {@link GENAI_SEMCONV_ATTRIBUTES}.
|
|
19
|
+
*
|
|
20
|
+
* Everything here is additive: the kernel's own names (`namzu.*`, and the
|
|
21
|
+
* `gen_ai.*` names it already wrote) are untouched. One process-wide switch
|
|
22
|
+
* turns the new names off for a backend that bills by attribute.
|
|
23
|
+
*/
|
|
24
|
+
export const GENAI_SEMCONV_VERSION = '1.41.1'
|
|
25
|
+
|
|
26
|
+
/** Schema URL of the pinned version; set on the tracer's instrumentation scope. */
|
|
27
|
+
export const GENAI_SEMCONV_SCHEMA_URL = `https://opentelemetry.io/schemas/${GENAI_SEMCONV_VERSION}`
|
|
28
|
+
|
|
29
|
+
/** `gen_ai.operation.name` values this kernel uses; all three are in the pinned convention's list. */
|
|
30
|
+
export const GENAI_OPERATION = {
|
|
31
|
+
CHAT: 'chat',
|
|
32
|
+
EXECUTE_TOOL: 'execute_tool',
|
|
33
|
+
INVOKE_AGENT: 'invoke_agent',
|
|
34
|
+
} as const
|
|
35
|
+
|
|
36
|
+
/** The convention's name for the error class of a failed operation (stable). */
|
|
37
|
+
export const ERROR_TYPE = 'error.type'
|
|
38
|
+
|
|
39
|
+
/** The convention's fallback `error.type` value. */
|
|
40
|
+
export const ERROR_TYPE_OTHER = '_OTHER'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Every attribute name the pinned convention version defines for the three
|
|
44
|
+
* span kinds this kernel emits (inference, execute tool, invoke agent) that
|
|
45
|
+
* the kernel may write, whether the kernel's own earlier code or the additions
|
|
46
|
+
* wrote it. A span's `gen_ai.*` / `error.type` keys must all be in here, with
|
|
47
|
+
* the one exception named in {@link GENAI_NON_CONVENTION_NAMES}.
|
|
48
|
+
*/
|
|
49
|
+
export const GENAI_SEMCONV_ATTRIBUTES: readonly string[] = [
|
|
50
|
+
GENAI.OPERATION_NAME,
|
|
51
|
+
GENAI.PROVIDER_NAME,
|
|
52
|
+
ERROR_TYPE,
|
|
53
|
+
GENAI.CONVERSATION_ID,
|
|
54
|
+
GENAI.AGENT_NAME,
|
|
55
|
+
GENAI.AGENT_ID,
|
|
56
|
+
GENAI.REQUEST_MODEL,
|
|
57
|
+
GENAI.REQUEST_STREAM,
|
|
58
|
+
GENAI.REQUEST_TEMPERATURE,
|
|
59
|
+
GENAI.REQUEST_MAX_TOKENS,
|
|
60
|
+
GENAI.RESPONSE_MODEL,
|
|
61
|
+
GENAI.RESPONSE_ID,
|
|
62
|
+
GENAI.RESPONSE_FINISH_REASONS,
|
|
63
|
+
GENAI.USAGE_INPUT_TOKENS,
|
|
64
|
+
GENAI.USAGE_OUTPUT_TOKENS,
|
|
65
|
+
GENAI.USAGE_CACHE_READ_INPUT_TOKENS,
|
|
66
|
+
GENAI.USAGE_CACHE_CREATION_INPUT_TOKENS,
|
|
67
|
+
GENAI.USAGE_REASONING_OUTPUT_TOKENS,
|
|
68
|
+
GENAI.TOOL_NAME,
|
|
69
|
+
GENAI.TOOL_CALL_ID,
|
|
70
|
+
GENAI.TOOL_TYPE,
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `gen_ai.*` names the kernel writes that are NOT in the pinned convention.
|
|
75
|
+
* `gen_ai.system` is still in the 1.41.1 registry but deprecated, replaced by
|
|
76
|
+
* `gen_ai.provider.name`; the kernel emits it deliberately and
|
|
77
|
+
* it stays because it is existing output that nothing may remove.
|
|
78
|
+
*/
|
|
79
|
+
export const GENAI_NON_CONVENTION_NAMES: readonly string[] = [GENAI.SYSTEM]
|
|
80
|
+
|
|
81
|
+
let enabled = true
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Turn the convention's attribute names (and the schema URL) on or off for
|
|
85
|
+
* every span the kernel starts from now on. On by default. `@namzu/telemetry`
|
|
86
|
+
* calls this from `TelemetryConfig.genAiAttributes`; a host that does not use
|
|
87
|
+
* that package can call it directly.
|
|
88
|
+
*/
|
|
89
|
+
export function setGenAiConventions(on: boolean): void {
|
|
90
|
+
enabled = on
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function genAiConventionsEnabled(): boolean {
|
|
94
|
+
return enabled
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The value for `gen_ai.provider.name`, which the convention requires on every
|
|
99
|
+
* span that carries it.
|
|
100
|
+
*
|
|
101
|
+
* The convention (1.41.1) says: "If one of them applies, then the respective
|
|
102
|
+
* value MUST be used; otherwise, a custom value MAY be used." and states no
|
|
103
|
+
* further rule on the form of a custom value. So:
|
|
104
|
+
*
|
|
105
|
+
* - a listed value only when our provider IS that vendor's own API (the
|
|
106
|
+
* cases below; `google` is `gcp.gen_ai`, "Any Google generative AI
|
|
107
|
+
* endpoint", since our id does not say which one);
|
|
108
|
+
* - otherwise our own provider id, lower case, as the custom value. A gateway
|
|
109
|
+
* that forwards to several vendors, a local runner and a custom HTTP
|
|
110
|
+
* endpoint are never mapped to a vendor they are not.
|
|
111
|
+
*
|
|
112
|
+
* `undefined` only when there is no provider id at all.
|
|
113
|
+
*/
|
|
114
|
+
export function genAiProviderName(providerId: string | undefined): string | undefined {
|
|
115
|
+
switch (providerId) {
|
|
116
|
+
case 'anthropic':
|
|
117
|
+
return 'anthropic'
|
|
118
|
+
case 'bedrock':
|
|
119
|
+
return 'aws.bedrock'
|
|
120
|
+
case 'deepseek':
|
|
121
|
+
return 'deepseek'
|
|
122
|
+
case 'google':
|
|
123
|
+
return 'gcp.gen_ai'
|
|
124
|
+
case 'openai':
|
|
125
|
+
case 'codex':
|
|
126
|
+
return 'openai'
|
|
127
|
+
default: {
|
|
128
|
+
const own = providerId?.trim().toLowerCase()
|
|
129
|
+
return own ? own : undefined
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A low-cardinality class for a failure, for `error.type`: the provider
|
|
136
|
+
* error kind for a classified provider failure, the error's own class name
|
|
137
|
+
* for an exception, `_OTHER` for anything else (a thrown string, a plain
|
|
138
|
+
* `Error`).
|
|
139
|
+
*/
|
|
140
|
+
export function genAiErrorType(err: unknown): string {
|
|
141
|
+
if (isProviderRequestError(err)) return err.kind
|
|
142
|
+
if (err instanceof Error) {
|
|
143
|
+
const name = err.name
|
|
144
|
+
return name && name !== 'Error' ? name : ERROR_TYPE_OTHER
|
|
145
|
+
}
|
|
146
|
+
return ERROR_TYPE_OTHER
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Added when a turn (an agent run, a sub-agent included) starts. */
|
|
150
|
+
export function agentRunAttributes(providerId: string | undefined): Attributes {
|
|
151
|
+
if (!enabled) return {}
|
|
152
|
+
const provider = genAiProviderName(providerId)
|
|
153
|
+
return {
|
|
154
|
+
[GENAI.OPERATION_NAME]: GENAI_OPERATION.INVOKE_AGENT,
|
|
155
|
+
...(provider !== undefined ? { [GENAI.PROVIDER_NAME]: provider } : {}),
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Added when a model call starts. Every model call is streamed. */
|
|
160
|
+
export function chatStartAttributes(providerId: string | undefined): Attributes {
|
|
161
|
+
if (!enabled) return {}
|
|
162
|
+
const provider = genAiProviderName(providerId)
|
|
163
|
+
return {
|
|
164
|
+
...(provider !== undefined ? { [GENAI.PROVIDER_NAME]: provider } : {}),
|
|
165
|
+
[GENAI.REQUEST_STREAM]: true,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Added when a model call answers. A count is written only when the driver
|
|
171
|
+
* reported a positive one: the record cannot tell "none" from "not
|
|
172
|
+
* reported", so a zero is left out rather than stated.
|
|
173
|
+
*/
|
|
174
|
+
export function chatUsageAttributes(usage: TokenUsage): Attributes {
|
|
175
|
+
if (!enabled) return {}
|
|
176
|
+
return {
|
|
177
|
+
...(usage.cachedTokens > 0
|
|
178
|
+
? { [GENAI.USAGE_CACHE_READ_INPUT_TOKENS]: usage.cachedTokens }
|
|
179
|
+
: {}),
|
|
180
|
+
...(usage.cacheWriteTokens > 0
|
|
181
|
+
? { [GENAI.USAGE_CACHE_CREATION_INPUT_TOKENS]: usage.cacheWriteTokens }
|
|
182
|
+
: {}),
|
|
183
|
+
...(usage.reasoningTokens !== undefined && usage.reasoningTokens > 0
|
|
184
|
+
? { [GENAI.USAGE_REASONING_OUTPUT_TOKENS]: usage.reasoningTokens }
|
|
185
|
+
: {}),
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Added when a tool call starts. */
|
|
190
|
+
export function toolCallAttributes(): Attributes {
|
|
191
|
+
if (!enabled) return {}
|
|
192
|
+
return { [GENAI.OPERATION_NAME]: GENAI_OPERATION.EXECUTE_TOOL }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The `error.type` of a tool call the kernel refused (not available, not allowed, plan mode); a custom low-cardinality value, which the convention allows. */
|
|
196
|
+
export const PERMISSION_DENIED = 'permission_denied'
|
|
197
|
+
|
|
198
|
+
/** Added to a span whose operation ended in an error. */
|
|
199
|
+
export function errorTypeAttributes(errorType: string): Attributes {
|
|
200
|
+
if (!enabled) return {}
|
|
201
|
+
return { [ERROR_TYPE]: errorType }
|
|
202
|
+
}
|
|
@@ -9,9 +9,19 @@
|
|
|
9
9
|
// discarded — standard OTEL library behavior.
|
|
10
10
|
|
|
11
11
|
import { type SpanContext, type Tracer, context, trace } from '@opentelemetry/api'
|
|
12
|
+
import { GENAI_SEMCONV_SCHEMA_URL, genAiConventionsEnabled } from './genai.js'
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* The kernel's tracer. While the GenAI convention names are on, its
|
|
16
|
+
* instrumentation scope carries the schema URL of the pinned convention
|
|
17
|
+
* version, which is how the convention says a producer states the version.
|
|
18
|
+
*/
|
|
13
19
|
export function getTracer(): Tracer {
|
|
14
|
-
return
|
|
20
|
+
return genAiConventionsEnabled()
|
|
21
|
+
? trace
|
|
22
|
+
.getTracerProvider()
|
|
23
|
+
.getTracer('namzu', undefined, { schemaUrl: GENAI_SEMCONV_SCHEMA_URL })
|
|
24
|
+
: trace.getTracer('namzu')
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
/**
|
|
@@ -57,3 +67,32 @@ export function getActiveSpanContext(): SpanContext | undefined {
|
|
|
57
67
|
return undefined
|
|
58
68
|
}
|
|
59
69
|
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The active span as the W3C trace context an outgoing request can carry:
|
|
73
|
+
* `{ traceparent, tracestate? }`, or `{}` when no span is active.
|
|
74
|
+
*
|
|
75
|
+
* `{}` is what a host that never registered telemetry gets, for the same
|
|
76
|
+
* reason {@link getActiveSpanContext} returns `undefined` there: nothing is
|
|
77
|
+
* special-cased, the api's default context has no span. A span context that
|
|
78
|
+
* is not valid W3C (an all-zero id, a wrong length) also yields `{}`, since a
|
|
79
|
+
* malformed `traceparent` is worse than none.
|
|
80
|
+
*/
|
|
81
|
+
export function getTraceContextHeaders(): { traceparent?: string; tracestate?: string } {
|
|
82
|
+
const span = getActiveSpanContext()
|
|
83
|
+
if (!span) return {}
|
|
84
|
+
const { traceId, spanId } = span
|
|
85
|
+
if (!/^[0-9a-f]{32}$/.test(traceId) || /^0+$/.test(traceId)) return {}
|
|
86
|
+
if (!/^[0-9a-f]{16}$/.test(spanId) || /^0+$/.test(spanId)) return {}
|
|
87
|
+
const flags = (span.traceFlags & 0xff).toString(16).padStart(2, '0')
|
|
88
|
+
let tracestate: string | undefined
|
|
89
|
+
try {
|
|
90
|
+
tracestate = span.traceState?.serialize() || undefined
|
|
91
|
+
} catch {
|
|
92
|
+
tracestate = undefined
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
traceparent: `00-${traceId}-${spanId}-${flags}`,
|
|
96
|
+
...(tracestate ? { tracestate } : {}),
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import type { SessionGoalStore } from '../../store/goal/index.js'
|
|
2
|
+
import { StaleGoalError } from '../../store/goal/index.js'
|
|
3
|
+
import type { GoalConfirmedBy, GoalRoundAuthority, SessionGoal } from '../../types/goal/index.js'
|
|
4
|
+
import type { HandBack } from '../../types/session/hand-back.js'
|
|
5
|
+
import type { TurnOutcome } from '../../types/session/outcome.js'
|
|
6
|
+
import { showCommand } from '../coordinator/outcome-record.js'
|
|
7
|
+
import { handBackKindSentence, handBackReason } from '../hand-back/index.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Ending a goal by the record of the work.
|
|
11
|
+
*
|
|
12
|
+
* Today the model's own report ends a goal. With `confirmedBy: 'record'` the
|
|
13
|
+
* kernel first reads the record it keeps of the work (`Turn.outcome`, built
|
|
14
|
+
* from what the tool calls did and never from what the model says) and the
|
|
15
|
+
* goal ends only as that record allows. There is no judge model and no model
|
|
16
|
+
* call beyond the turns themselves: the decision is a pure function over what
|
|
17
|
+
* the model reported, the record and the count of refusals so far.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Times the record may send a goal back to the model before the goal ends as
|
|
22
|
+
* not confirmed. Three: a first refusal normally costs one fix and one rerun of
|
|
23
|
+
* the check, a second covers a fix that did not hold, a third a check that
|
|
24
|
+
* needed a second look; past that the model is looping, and every further
|
|
25
|
+
* refusal costs a model call that has stopped moving the work. The limit is
|
|
26
|
+
* per goal and a host can set its own.
|
|
27
|
+
*/
|
|
28
|
+
export const DEFAULT_GOAL_REFUSALS = 3
|
|
29
|
+
|
|
30
|
+
/** The `reasonCode` that makes a `complete` report say that no check exists to run. */
|
|
31
|
+
export const NO_CHECK_REASON_CODE = 'no-check-available'
|
|
32
|
+
|
|
33
|
+
/** How a goal ended once the record had been read. */
|
|
34
|
+
export type GoalEnding =
|
|
35
|
+
/** A check passed and nothing changed after it. */
|
|
36
|
+
| 'confirmed'
|
|
37
|
+
/** The model said it was done; the record cannot confirm or contradict it. */
|
|
38
|
+
| 'stated-not-checked'
|
|
39
|
+
/** The record disagrees and the model could not or would not put it right. */
|
|
40
|
+
| 'not-confirmed'
|
|
41
|
+
|
|
42
|
+
/** What the model reported. */
|
|
43
|
+
export type GoalStatement =
|
|
44
|
+
/** The goal is met. */
|
|
45
|
+
| 'met'
|
|
46
|
+
/** The goal is met, and no check exists to run. */
|
|
47
|
+
| 'no-check'
|
|
48
|
+
/** The goal is not met: the model could not make the check pass. */
|
|
49
|
+
| 'could-not'
|
|
50
|
+
|
|
51
|
+
export type GoalDecision =
|
|
52
|
+
| { readonly kind: 'end'; readonly ending: GoalEnding; readonly sentence: string }
|
|
53
|
+
| { readonly kind: 'refuse'; readonly sentence: string; readonly note: string }
|
|
54
|
+
|
|
55
|
+
export interface GoalDecisionInput {
|
|
56
|
+
readonly statement: GoalStatement
|
|
57
|
+
/** The record of the turn the report was made in; `undefined` when none was kept. */
|
|
58
|
+
readonly record: TurnOutcome | undefined
|
|
59
|
+
/** Refusals already given to this goal. */
|
|
60
|
+
readonly refusals: number
|
|
61
|
+
/** Most refusals this goal may be given. */
|
|
62
|
+
readonly maxRefusals: number
|
|
63
|
+
/**
|
|
64
|
+
* An earlier round of this same goal changed files. Never makes a goal
|
|
65
|
+
* confirmed; it only keeps a round that changed nothing and ran nothing from
|
|
66
|
+
* being read as proof that nothing needed checking.
|
|
67
|
+
*/
|
|
68
|
+
readonly earlierRoundsChanged?: boolean
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The record in plain words, without a lead and without a final stop. */
|
|
72
|
+
export function recordSentence(record: TurnOutcome | undefined): string {
|
|
73
|
+
if (record === undefined) return 'there is no record of this work'
|
|
74
|
+
const named = (command: string | undefined): string =>
|
|
75
|
+
command === undefined ? 'a check' : (showCommand(command) ?? 'a command')
|
|
76
|
+
const passedCheck = [...record.checks].reverse().find((check) => check.result === 'passed')
|
|
77
|
+
switch (record.verdict) {
|
|
78
|
+
case 'checked':
|
|
79
|
+
return `${named(passedCheck?.command)} passed and nothing changed after it`
|
|
80
|
+
case 'unchanged':
|
|
81
|
+
return passedCheck
|
|
82
|
+
? `${named(passedCheck.command)} passed and nothing changed`
|
|
83
|
+
: 'nothing changed and no check ran'
|
|
84
|
+
case 'stale':
|
|
85
|
+
return 'files changed after the last passing check'
|
|
86
|
+
case 'failing': {
|
|
87
|
+
const failed = record.checks.filter((check) => check.result === 'failed')
|
|
88
|
+
const more = failed.length > 1 ? ` (and ${failed.length - 1} more)` : ''
|
|
89
|
+
return `${named(failed[0]?.command)} failed${more}`
|
|
90
|
+
}
|
|
91
|
+
case 'unchecked':
|
|
92
|
+
return 'files changed and no check ran'
|
|
93
|
+
case 'inconclusive': {
|
|
94
|
+
const masked = record.checks.find((check) => check.result === 'masked')
|
|
95
|
+
if (masked) {
|
|
96
|
+
const how = /(?<!\|)\|(?!\|)/.test(masked.command) ? 'the pipe' : 'the command line'
|
|
97
|
+
return `${named(masked.command)} ran, but ${how} hides whether it passed`
|
|
98
|
+
}
|
|
99
|
+
const unsettled = record.checks.find((check) => check.result === 'unsettled')
|
|
100
|
+
if (unsettled) return `${named(unsettled.command)} ran, but its result was not seen`
|
|
101
|
+
return 'files changed and no check gave a result that could be read'
|
|
102
|
+
}
|
|
103
|
+
case 'unknown':
|
|
104
|
+
return 'a command ran that may have changed files, and nothing could tell whether it did'
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const HOW_TO_SAY_SO =
|
|
109
|
+
'To say so, call update_goal with status "blocked" and the reason if the check cannot be made to pass, or with status "complete", reasonCode "no-check-available" and the reason if no check exists here.'
|
|
110
|
+
|
|
111
|
+
function refusalNote(record: TurnOutcome | undefined, earlierChanged: boolean): string {
|
|
112
|
+
switch (record?.verdict) {
|
|
113
|
+
case 'failing':
|
|
114
|
+
return `The record says ${recordSentence(record)}. Fix what it reports and run it again, or say plainly that you could not make it pass. ${HOW_TO_SAY_SO}`
|
|
115
|
+
case 'stale':
|
|
116
|
+
return `The record says ${recordSentence(record)}. Run the check again, or say plainly that you could not. ${HOW_TO_SAY_SO}`
|
|
117
|
+
case 'unchecked':
|
|
118
|
+
return `The record says ${recordSentence(record)}. Run the project's check on the files as they are now, or say plainly that no check can be run here. ${HOW_TO_SAY_SO}`
|
|
119
|
+
case 'inconclusive':
|
|
120
|
+
return `The record says ${recordSentence(record)}. Run the check again on its own so that its result can be read, or say plainly that you could not. ${HOW_TO_SAY_SO}`
|
|
121
|
+
default:
|
|
122
|
+
return earlierChanged
|
|
123
|
+
? `The record of this round says ${recordSentence(record)}, and an earlier round of this goal changed files. Run the project's check on the files as they are now, or say plainly that you could not. ${HOW_TO_SAY_SO}`
|
|
124
|
+
: `The record says ${recordSentence(record)}. Run the check, or say plainly that you could not. ${HOW_TO_SAY_SO}`
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* What to do with the model's report of a goal, given the record.
|
|
130
|
+
*
|
|
131
|
+
* - The model says it could not: the goal ends `not-confirmed`; it is never
|
|
132
|
+
* pushed again.
|
|
133
|
+
* - The model says no check exists: `stated-not-checked`, unless the record
|
|
134
|
+
* holds a failing or stale check (`not-confirmed`) or a passing one
|
|
135
|
+
* (`confirmed`).
|
|
136
|
+
* - The model says it is met: a passing check with nothing changed after it is
|
|
137
|
+
* `confirmed`; a failing check, files changed after the last pass, files
|
|
138
|
+
* changed and no check, or a check whose result could not be read are refused
|
|
139
|
+
* while refusals remain and `not-confirmed` once they do not; nothing
|
|
140
|
+
* changed, a command that may have changed files (`unknown`) or no record at
|
|
141
|
+
* all is `stated-not-checked`. `unknown` is not refused on purpose: the
|
|
142
|
+
* record keeps it for the whole turn, so no later check could clear it and a
|
|
143
|
+
* refusal would only cost calls until the bound.
|
|
144
|
+
*/
|
|
145
|
+
export function decideGoalConfirmation(input: GoalDecisionInput): GoalDecision {
|
|
146
|
+
const { statement, record, refusals, maxRefusals } = input
|
|
147
|
+
const sentence = recordSentence(record)
|
|
148
|
+
if (statement === 'could-not') return { kind: 'end', ending: 'not-confirmed', sentence }
|
|
149
|
+
|
|
150
|
+
const verdict = record?.verdict
|
|
151
|
+
if (verdict === 'checked') return { kind: 'end', ending: 'confirmed', sentence }
|
|
152
|
+
if (
|
|
153
|
+
verdict === 'unchanged' &&
|
|
154
|
+
record?.checks.some((check) => check.result === 'passed' && check.fresh !== false)
|
|
155
|
+
) {
|
|
156
|
+
return { kind: 'end', ending: 'confirmed', sentence }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const contradicted = verdict === 'failing' || verdict === 'stale'
|
|
160
|
+
if (statement === 'no-check') {
|
|
161
|
+
return contradicted
|
|
162
|
+
? { kind: 'end', ending: 'not-confirmed', sentence }
|
|
163
|
+
: { kind: 'end', ending: 'stated-not-checked', sentence }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const earlierChanged = input.earlierRoundsChanged === true
|
|
167
|
+
const refusable =
|
|
168
|
+
contradicted ||
|
|
169
|
+
verdict === 'unchecked' ||
|
|
170
|
+
verdict === 'inconclusive' ||
|
|
171
|
+
(verdict === 'unchanged' && earlierChanged)
|
|
172
|
+
if (!refusable) return { kind: 'end', ending: 'stated-not-checked', sentence }
|
|
173
|
+
if (refusals >= Math.max(0, maxRefusals)) {
|
|
174
|
+
return { kind: 'end', ending: 'not-confirmed', sentence }
|
|
175
|
+
}
|
|
176
|
+
return { kind: 'refuse', sentence, note: refusalNote(record, earlierChanged) }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** How a goal ended, for the line a person reads and for a machine reading the result. */
|
|
180
|
+
export interface GoalEndingReport {
|
|
181
|
+
readonly goalId: string
|
|
182
|
+
readonly ending: GoalEnding
|
|
183
|
+
/** The record in plain words, as {@link recordSentence} writes it. */
|
|
184
|
+
readonly sentence: string
|
|
185
|
+
/** How many times the record sent this goal back before it ended. */
|
|
186
|
+
readonly refusals: number
|
|
187
|
+
/**
|
|
188
|
+
* Set when the goal ended because the model handed the task back to a person
|
|
189
|
+
* (`hand_back_task`). The ending is `not-confirmed` and `sentence` is the
|
|
190
|
+
* kernel's sentence for the kind; the model's own words are in the hand-back.
|
|
191
|
+
*/
|
|
192
|
+
readonly handedBack?: HandBack
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The closing line of a goal: which of the three endings it was and, always, what the record held. */
|
|
196
|
+
export function describeGoalEnding(
|
|
197
|
+
report: Pick<GoalEndingReport, 'ending' | 'sentence' | 'handedBack'>,
|
|
198
|
+
): string {
|
|
199
|
+
if (report.handedBack) {
|
|
200
|
+
return `Goal not confirmed: the model handed the task back to a person (${report.sentence}).`
|
|
201
|
+
}
|
|
202
|
+
switch (report.ending) {
|
|
203
|
+
case 'confirmed':
|
|
204
|
+
return `Goal confirmed by the record: ${report.sentence}.`
|
|
205
|
+
case 'stated-not-checked':
|
|
206
|
+
return `Goal stated, not checked: the record cannot confirm it (${report.sentence}).`
|
|
207
|
+
case 'not-confirmed':
|
|
208
|
+
return `Goal not confirmed: the record says ${report.sentence}.`
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface GoalConfirmationOptions {
|
|
213
|
+
readonly store: SessionGoalStore
|
|
214
|
+
/** The same resolver the goal tools use: the authority of the turn, or none. */
|
|
215
|
+
readonly resolveAuthority: (turnId: string) => GoalRoundAuthority | null | undefined
|
|
216
|
+
/** What a goal that did not choose is confirmed by. Default `model`, the behaviour before this existed. */
|
|
217
|
+
readonly defaultConfirmedBy?: GoalConfirmedBy
|
|
218
|
+
/** Refusals a goal may be given. Default {@link DEFAULT_GOAL_REFUSALS}. */
|
|
219
|
+
readonly maxRefusals?: number
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface Claim {
|
|
223
|
+
readonly statement: GoalStatement
|
|
224
|
+
readonly reason?: string
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Holds, for one session, what the model reported this turn, how many times a
|
|
229
|
+
* goal was sent back, and whether an earlier round of it changed files. All of
|
|
230
|
+
* it lives in memory: a restart begins the count again, which is the cautious
|
|
231
|
+
* side of the bound only in that the model gets its refusals again.
|
|
232
|
+
*/
|
|
233
|
+
export class GoalConfirmation {
|
|
234
|
+
private readonly claims = new Map<string, Claim>()
|
|
235
|
+
private readonly refusals = new Map<string, number>()
|
|
236
|
+
private readonly changed = new Set<string>()
|
|
237
|
+
private readonly endings = new Map<string, GoalEndingReport>()
|
|
238
|
+
|
|
239
|
+
constructor(private readonly options: GoalConfirmationOptions) {}
|
|
240
|
+
|
|
241
|
+
/** What decides this goal: its own choice, else the host's setting, else the model. */
|
|
242
|
+
confirmedByFor(goal: Pick<SessionGoal, 'confirmedBy'>): GoalConfirmedBy {
|
|
243
|
+
return goal.confirmedBy ?? this.options.defaultConfirmedBy ?? 'model'
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
get maxRefusals(): number {
|
|
247
|
+
return this.options.maxRefusals ?? DEFAULT_GOAL_REFUSALS
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Refusals given to a goal so far. */
|
|
251
|
+
refusalsFor(goalId: string): number {
|
|
252
|
+
return this.refusals.get(goalId) ?? 0
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The model reported on the goal in the round now running; the record is read
|
|
257
|
+
* when it finishes its answer. Held by goal, because only one round of a goal
|
|
258
|
+
* runs at a time and a round that ends without an answer leaves the goal
|
|
259
|
+
* active: the next report replaces this one.
|
|
260
|
+
*/
|
|
261
|
+
claim(goalId: string, statement: GoalStatement, reason?: string): void {
|
|
262
|
+
this.claims.set(goalId, { statement, ...(reason !== undefined ? { reason } : {}) })
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** The ending the turn's goal came to, once; `undefined` when it did not end here. */
|
|
266
|
+
takeEnding(turnId: string): GoalEndingReport | undefined {
|
|
267
|
+
const ending = this.endings.get(turnId)
|
|
268
|
+
this.endings.delete(turnId)
|
|
269
|
+
return ending
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The model handed this round's task back to a person: the goal, when this
|
|
274
|
+
* turn is a round of an active one, ends as not confirmed and is left blocked
|
|
275
|
+
* with the reason, whatever it was confirmed by. Nothing for any other turn.
|
|
276
|
+
*/
|
|
277
|
+
async handBack(turnId: string, handBack: HandBack): Promise<GoalEndingReport | undefined> {
|
|
278
|
+
const authority = this.options.resolveAuthority(turnId)
|
|
279
|
+
if (!authority) return undefined
|
|
280
|
+
const goal = await this.options.store.getGoal(authority.sessionId, authority.tenantId)
|
|
281
|
+
if (!goal || goal.id !== authority.id || goal.phase !== 'active') return undefined
|
|
282
|
+
const ref = { id: authority.id, revision: authority.revision }
|
|
283
|
+
try {
|
|
284
|
+
await this.options.store.blockGoal(authority.sessionId, authority.tenantId, ref, {
|
|
285
|
+
code: 'handed-back-to-a-person',
|
|
286
|
+
message: handBackReason(handBack),
|
|
287
|
+
})
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (error instanceof StaleGoalError) return undefined
|
|
290
|
+
throw error
|
|
291
|
+
}
|
|
292
|
+
const report: GoalEndingReport = {
|
|
293
|
+
goalId: goal.id,
|
|
294
|
+
ending: 'not-confirmed',
|
|
295
|
+
sentence: handBackKindSentence(handBack.kind),
|
|
296
|
+
refusals: this.refusalsFor(goal.id),
|
|
297
|
+
handedBack: handBack,
|
|
298
|
+
}
|
|
299
|
+
this.claims.delete(goal.id)
|
|
300
|
+
this.refusals.delete(goal.id)
|
|
301
|
+
this.changed.delete(goal.id)
|
|
302
|
+
return report
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Drop a report that was never answered for, when its round ends without the turn finishing an answer. */
|
|
306
|
+
forget(goalId: string): void {
|
|
307
|
+
this.claims.delete(goalId)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* For `TurnConfig.outcome.beforeEnd`: the note that sends the model back to
|
|
312
|
+
* the work, or `undefined` when the turn may end. A turn that is not a round
|
|
313
|
+
* of a goal confirmed by the record is never touched.
|
|
314
|
+
*/
|
|
315
|
+
beforeEnd(
|
|
316
|
+
turnId: string,
|
|
317
|
+
): (read: () => Promise<TurnOutcome | undefined>) => Promise<string | undefined> {
|
|
318
|
+
return async (read) => {
|
|
319
|
+
const authority = this.options.resolveAuthority(turnId)
|
|
320
|
+
if (!authority) return undefined
|
|
321
|
+
const goal = await this.options.store.getGoal(authority.sessionId, authority.tenantId)
|
|
322
|
+
if (!goal || goal.id !== authority.id || goal.phase !== 'active') return undefined
|
|
323
|
+
if (this.confirmedByFor(goal) !== 'record') return undefined
|
|
324
|
+
|
|
325
|
+
const record = await read()
|
|
326
|
+
const claim = this.claims.get(goal.id)
|
|
327
|
+
this.claims.delete(goal.id)
|
|
328
|
+
const changedHere = (record?.changes.total ?? 0) > 0 || record?.changes.outsideKnownTools
|
|
329
|
+
if (!claim) {
|
|
330
|
+
if (changedHere) this.changed.add(goal.id)
|
|
331
|
+
return undefined
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const decision = decideGoalConfirmation({
|
|
335
|
+
statement: claim.statement,
|
|
336
|
+
record,
|
|
337
|
+
refusals: this.refusalsFor(goal.id),
|
|
338
|
+
maxRefusals: this.maxRefusals,
|
|
339
|
+
earlierRoundsChanged: this.changed.has(goal.id),
|
|
340
|
+
})
|
|
341
|
+
if (changedHere) this.changed.add(goal.id)
|
|
342
|
+
if (decision.kind === 'refuse') {
|
|
343
|
+
this.refusals.set(goal.id, this.refusalsFor(goal.id) + 1)
|
|
344
|
+
return decision.note
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const ref = { id: authority.id, revision: authority.revision }
|
|
348
|
+
try {
|
|
349
|
+
if (decision.ending === 'not-confirmed') {
|
|
350
|
+
const said = claim.reason ? ` The model said: ${claim.reason}` : ''
|
|
351
|
+
await this.options.store.blockGoal(authority.sessionId, authority.tenantId, ref, {
|
|
352
|
+
code: 'not-confirmed-by-record',
|
|
353
|
+
message: `The record says ${decision.sentence}.${said}`,
|
|
354
|
+
})
|
|
355
|
+
} else {
|
|
356
|
+
await this.options.store.completeGoal(authority.sessionId, authority.tenantId, ref)
|
|
357
|
+
}
|
|
358
|
+
} catch (error) {
|
|
359
|
+
// The person changed the goal while the model was answering; what they
|
|
360
|
+
// did stands, and nothing is said about an ending that did not happen.
|
|
361
|
+
if (error instanceof StaleGoalError) return undefined
|
|
362
|
+
throw error
|
|
363
|
+
}
|
|
364
|
+
this.endings.set(turnId, {
|
|
365
|
+
goalId: goal.id,
|
|
366
|
+
ending: decision.ending,
|
|
367
|
+
sentence: decision.sentence,
|
|
368
|
+
refusals: this.refusalsFor(goal.id),
|
|
369
|
+
})
|
|
370
|
+
this.refusals.delete(goal.id)
|
|
371
|
+
this.changed.delete(goal.id)
|
|
372
|
+
return undefined
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
package/src/tools/goal/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { GoalRoundAuthority, SessionGoal } from '../../types/goal/index.js'
|
|
|
5
5
|
import type { TurnId } from '../../types/ids/index.js'
|
|
6
6
|
import type { ToolDefinition } from '../../types/tool/index.js'
|
|
7
7
|
import { defineTool } from '../defineTool.js'
|
|
8
|
+
import { type GoalConfirmation, NO_CHECK_REASON_CODE } from './confirmation.js'
|
|
8
9
|
|
|
9
10
|
export const SESSION_GOAL_TOOL_NAMES = ['get_goal', 'update_goal'] as const
|
|
10
11
|
export const MIN_GOAL_BLOCK_ROUND = 3
|
|
@@ -58,6 +59,7 @@ function authorityFor(
|
|
|
58
59
|
export function buildSessionGoalTools(
|
|
59
60
|
store: SessionGoalStore,
|
|
60
61
|
resolveAuthority: ResolveGoalRoundAuthority,
|
|
62
|
+
options: { readonly confirmation?: GoalConfirmation } = {},
|
|
61
63
|
): ToolDefinition[] {
|
|
62
64
|
const getGoal = defineTool({
|
|
63
65
|
name: 'get_goal',
|
|
@@ -106,8 +108,42 @@ export function buildSessionGoalTools(
|
|
|
106
108
|
concurrencySafe: false,
|
|
107
109
|
async execute({ status, reasonCode, reason }, context) {
|
|
108
110
|
const authority = authorityFor(resolveAuthority, context.turnId)
|
|
109
|
-
await currentGoal(store, authority)
|
|
111
|
+
const current = await currentGoal(store, authority)
|
|
110
112
|
const ref = { id: authority.id, revision: authority.revision }
|
|
113
|
+
const confirmation = options.confirmation
|
|
114
|
+
if (confirmation && confirmation.confirmedByFor(current) === 'record') {
|
|
115
|
+
// The report is held until the model finishes its answer; the kernel then
|
|
116
|
+
// reads the record of the work and the goal ends as the record allows.
|
|
117
|
+
if (status === 'complete') {
|
|
118
|
+
confirmation.claim(
|
|
119
|
+
authority.id,
|
|
120
|
+
reasonCode === NO_CHECK_REASON_CODE ? 'no-check' : 'met',
|
|
121
|
+
reason,
|
|
122
|
+
)
|
|
123
|
+
return {
|
|
124
|
+
success: true,
|
|
125
|
+
output:
|
|
126
|
+
'Completion reported. This goal is confirmed by the record: when you finish your answer the kernel reads the record of the work done, and the goal ends as that record allows. Give your final answer now.',
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (
|
|
130
|
+
authority.round < MIN_GOAL_BLOCK_ROUND &&
|
|
131
|
+
confirmation.refusalsFor(authority.id) === 0
|
|
132
|
+
) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`A goal cannot be marked blocked before admitted round ${MIN_GOAL_BLOCK_ROUND}, unless the record has already sent it back once; this is round ${authority.round}.`,
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
if (!reasonCode || !reason) {
|
|
138
|
+
throw new TypeError('Blocked goal updates require reasonCode and reason.')
|
|
139
|
+
}
|
|
140
|
+
confirmation.claim(authority.id, 'could-not', reason)
|
|
141
|
+
return {
|
|
142
|
+
success: true,
|
|
143
|
+
output:
|
|
144
|
+
'Reported as not met. The goal ends as not confirmed when you finish your answer, and the record of the work is shown with your reason. Give your final answer now.',
|
|
145
|
+
}
|
|
146
|
+
}
|
|
111
147
|
if (status === 'complete') {
|
|
112
148
|
const goal = await store.completeGoal(authority.sessionId, authority.tenantId, ref)
|
|
113
149
|
return { success: true, output: 'Goal marked complete.', data: goal }
|