@gajae-code/coding-agent 0.16.3 → 0.16.6
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 +113 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/args.d.ts +2 -2
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- package/dist/types/cli/root-flags.d.ts +4 -2
- package/dist/types/cli.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +31 -3
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +7 -0
- package/dist/types/discovery/claude-plugins.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +35 -0
- package/dist/types/exec/bash-executor.d.ts +17 -1
- package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
- package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-signals.d.ts +2 -0
- package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
- package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
- package/dist/types/exec/bash-shell-worker.d.ts +1 -0
- package/dist/types/exec/isolated-shell.d.ts +19 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
- package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +46 -6
- package/dist/types/extensibility/extensions/types.d.ts +16 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/extensibility/skill-management.d.ts +9 -4
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
- package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- package/dist/types/modes/components/settings-selector.d.ts +6 -1
- package/dist/types/modes/components/status-line/command.d.ts +35 -0
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -0
- package/dist/types/modes/components/tool-status-header.d.ts +14 -0
- package/dist/types/modes/controllers/event-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/runtime-mcp/client.d.ts +9 -1
- package/dist/types/runtime-mcp/manager.d.ts +7 -1
- package/dist/types/runtime-mcp/pool.d.ts +22 -3
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/runtime-mcp/types.d.ts +22 -0
- package/dist/types/sdk/broker/ensure.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
- package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
- package/dist/types/sdk/client/client.d.ts +6 -0
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/session-runtime.d.ts +26 -1
- package/dist/types/sdk/lifecycle/service.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +10 -2
- package/dist/types/sdk/session-api-key.d.ts +26 -0
- package/dist/types/sdk/session.d.ts +5 -1
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +11 -0
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/setup/paseo/announce.d.ts +112 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +10 -0
- package/dist/types/task/executor.d.ts +5 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +7 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +10 -0
- package/dist/types/tools/path-utils.d.ts +2 -0
- package/dist/types/tools/search.d.ts +6 -0
- package/package.json +9 -7
- package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
- package/scripts/generate-sdk-operation-inventory.ts +11 -0
- package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/args.ts +20 -9
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli/root-flags.ts +5 -3
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +379 -31
- package/src/config/model-preset-registry.ts +120 -19
- package/src/config/model-profiles.ts +40 -0
- package/src/config/model-registry.ts +73 -23
- package/src/config/model-resolver.ts +48 -0
- package/src/config/settings-schema.ts +33 -1
- package/src/config/settings.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +33 -2
- package/src/coordinator-mcp/server.ts +477 -101
- package/src/crash/sanitize.ts +1 -1
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +52 -55
- package/src/discovery/claude-plugins.ts +12 -4
- package/src/discovery/helpers.ts +307 -34
- package/src/exec/bash-executor.ts +330 -50
- package/src/exec/bash-shell-guardian.ts +388 -0
- package/src/exec/bash-shell-runtime-entry.ts +5 -0
- package/src/exec/bash-shell-signals.ts +29 -0
- package/src/exec/bash-shell-supervisor-entry.ts +3 -0
- package/src/exec/bash-shell-supervisor.ts +359 -0
- package/src/exec/bash-shell-worker-entry.ts +5 -0
- package/src/exec/bash-shell-worker-protocol.ts +36 -0
- package/src/exec/bash-shell-worker.ts +116 -0
- package/src/exec/isolated-shell.ts +393 -0
- package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
- package/src/extensibility/extensions/function-hooks.ts +959 -0
- package/src/extensibility/extensions/index.ts +22 -0
- package/src/extensibility/extensions/loader.ts +53 -2
- package/src/extensibility/extensions/runner.ts +895 -264
- package/src/extensibility/extensions/types.ts +42 -0
- package/src/extensibility/extensions/wrapper.ts +56 -15
- package/src/extensibility/gjc-plugins/compiler.ts +228 -60
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- package/src/extensibility/gjc-plugins/installer.ts +13 -12
- package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
- package/src/extensibility/gjc-plugins/paths.ts +13 -0
- package/src/extensibility/gjc-plugins/registry.ts +113 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
- package/src/extensibility/gjc-plugins/schema.ts +121 -4
- package/src/extensibility/gjc-plugins/types.ts +25 -0
- package/src/extensibility/hooks/loader.ts +133 -16
- package/src/extensibility/hooks/types.ts +28 -0
- package/src/extensibility/runtime-skill-discovery.ts +149 -190
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +73 -25
- package/src/gjc-runtime/empty-delete-gc.ts +94 -4
- package/src/gjc-runtime/gc-runtime.ts +29 -0
- package/src/gjc-runtime/linux-proc.ts +14 -0
- package/src/gjc-runtime/repository-binding.ts +1 -1
- package/src/gjc-runtime/session-state-lock.ts +143 -10
- package/src/gjc-runtime/session-state-sidecar.ts +27 -11
- package/src/gjc-runtime/state-writer.ts +30 -29
- package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
- package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -0
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +12 -12
- package/src/lsp/config.ts +10 -3
- package/src/lsp/index.ts +1 -1
- package/src/lsp/path-trust.ts +69 -14
- package/src/main.ts +78 -35
- package/src/modes/DESIGN.md +54 -0
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/assistant-message.ts +22 -9
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/model-selector.ts +166 -33
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/settings-selector.ts +1075 -235
- package/src/modes/components/status-line/command.ts +190 -0
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/segments.ts +32 -5
- package/src/modes/components/status-line/types.ts +5 -0
- package/src/modes/components/tool-status-header.ts +396 -22
- package/src/modes/controllers/command-controller.ts +3 -0
- package/src/modes/controllers/event-controller.ts +151 -7
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/controllers/selector-controller.ts +21 -0
- package/src/modes/interactive-mode.ts +53 -6
- package/src/modes/print-mode.ts +5 -1
- package/src/modes/prompt-action-autocomplete.ts +54 -27
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/types.ts +17 -3
- package/src/modes/utils/ui-helpers.ts +2 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/search.md +2 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/client.ts +60 -17
- package/src/runtime-mcp/manager.ts +189 -14
- package/src/runtime-mcp/pool.test.ts +218 -0
- package/src/runtime-mcp/pool.ts +224 -23
- package/src/runtime-mcp/transports/stdio.ts +158 -24
- package/src/runtime-mcp/types.ts +24 -0
- package/src/sdk/broker/broker.ts +124 -32
- package/src/sdk/broker/ensure.ts +248 -41
- package/src/sdk/broker/lifecycle-ledger.ts +7 -17
- package/src/sdk/broker/lifecycle.ts +56 -6
- package/src/sdk/broker/startup-failure.ts +15 -6
- package/src/sdk/bus/chat-daemon-control.ts +3 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
- package/src/sdk/bus/chat-effect-journal.ts +132 -7
- package/src/sdk/bus/conversation-store.ts +29 -0
- package/src/sdk/bus/lifecycle-commands.ts +17 -11
- package/src/sdk/bus/slack-conversation.ts +1 -0
- package/src/sdk/bus/slack-daemon.ts +478 -49
- package/src/sdk/bus/slack-live-provider.ts +12 -0
- package/src/sdk/bus/slack-provider.ts +5 -0
- package/src/sdk/bus/telegram-daemon.ts +16 -2
- package/src/sdk/bus/topic-registry.ts +19 -0
- package/src/sdk/cli/master-cli.ts +1 -1
- package/src/sdk/cli/session-cli.ts +11 -8
- package/src/sdk/client/client.ts +66 -17
- package/src/sdk/host/host.ts +29 -9
- package/src/sdk/host/query/revision-store.ts +19 -13
- package/src/sdk/host/reverse-leases.ts +7 -4
- package/src/sdk/host/session-runtime.test.ts +52 -10
- package/src/sdk/host/session-runtime.ts +383 -44
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +77 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session-api-key.ts +94 -0
- package/src/sdk/session.ts +228 -55
- package/src/sdk/transport/relay.ts +35 -13
- package/src/sdk/transport/serve-cli.ts +3 -3
- package/src/session/agent-session.ts +240 -90
- package/src/session/contribution-prep.ts +214 -21
- package/src/session/fallback-chain-controller.ts +38 -0
- package/src/session/internal/managed-session-storage.ts +19 -5
- package/src/session/messages.ts +5 -0
- package/src/session/session-manager.ts +29 -5
- package/src/session-import/redact.ts +5 -1
- package/src/setup/paseo/announce.ts +456 -0
- package/src/setup/paseo/setup-deps.ts +15 -3
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/system-prompt.ts +29 -3
- package/src/task/executor.ts +7 -1
- package/src/task/index.ts +21 -4
- package/src/tools/bash.ts +63 -19
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +29 -1
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +10 -0
- package/src/tools/job.ts +21 -4
- package/src/tools/monitor.ts +8 -2
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/read.ts +14 -2
- package/src/tools/render-utils.ts +1 -3
- package/src/tools/resource-gc.ts +28 -8
- package/src/tools/search.ts +116 -58
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +37 -2
- package/src/tools/subagent.ts +6 -3
- package/src/tools/tool-catalog.generated.ts +10 -3
- package/src/tools/ultragoal-ask-guard.ts +2 -2
- package/src/web/insane/bridge.ts +113 -23
- package/src/web/search/providers/perplexity.ts +102 -41
package/src/tools/skill.ts
CHANGED
|
@@ -32,6 +32,10 @@ import { getSkillManifest } from "../gjc-runtime/workflow-manifest";
|
|
|
32
32
|
import skillDescription from "../prompts/tools/skill.md" with { type: "text" };
|
|
33
33
|
import { SKILL_PROMPT_MESSAGE_TYPE } from "../session/messages";
|
|
34
34
|
import { isCanonicalGjcWorkflowSkill } from "../skill-state/active-state";
|
|
35
|
+
import {
|
|
36
|
+
getSkillFilesystemIdentity,
|
|
37
|
+
isCanonicalGjcWorkflowSkillFilesystemAlias,
|
|
38
|
+
} from "../skill-state/canonical-skills";
|
|
35
39
|
import type { ToolSession } from ".";
|
|
36
40
|
import { ToolError } from "./tool-errors";
|
|
37
41
|
|
|
@@ -66,6 +70,26 @@ function normalizeSkillName(name: string | undefined): string {
|
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
const SKILL_NAME_GLOB_PATTERN = /[*?[\]{}]/;
|
|
73
|
+
const MAX_AVAILABLE_SKILL_NAMES = 50;
|
|
74
|
+
const MAX_AVAILABLE_SKILL_CHARS = 2048;
|
|
75
|
+
|
|
76
|
+
function formatAvailableSkills(skills: readonly { name: string }[]): string {
|
|
77
|
+
const names = [...new Set(skills.map(skill => skill.name))].sort();
|
|
78
|
+
const visible: string[] = [];
|
|
79
|
+
let visibleChars = 0;
|
|
80
|
+
for (let index = 0; index < names.length && visible.length < MAX_AVAILABLE_SKILL_NAMES; index++) {
|
|
81
|
+
const name = names[index];
|
|
82
|
+
const separatorChars = visible.length > 0 ? 2 : 0;
|
|
83
|
+
const omittedAfterAdding = names.length - (visible.length + 1);
|
|
84
|
+
const markerChars = omittedAfterAdding > 0 ? `, … (+${omittedAfterAdding} more)`.length : 0;
|
|
85
|
+
if (visibleChars + separatorChars + name.length + markerChars > MAX_AVAILABLE_SKILL_CHARS) break;
|
|
86
|
+
visible.push(name);
|
|
87
|
+
visibleChars += separatorChars + name.length;
|
|
88
|
+
}
|
|
89
|
+
const omitted = names.length - visible.length;
|
|
90
|
+
const marker = omitted > 0 ? `… (+${omitted} more)` : "";
|
|
91
|
+
return visible.length > 0 && marker ? `${visible.join(", ")}, ${marker}` : visible.join(", ") || marker;
|
|
92
|
+
}
|
|
69
93
|
|
|
70
94
|
type SkillToolInput = z.infer<typeof skillSchema>;
|
|
71
95
|
|
|
@@ -137,6 +161,12 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
|
|
|
137
161
|
`skill tool: "${requestedName}" is not a valid skill name. The name must be a single concrete skill (e.g. "ralplan"), not a glob or wildcard pattern. Pass one exact skill name as shown in /skill:<name>.`,
|
|
138
162
|
);
|
|
139
163
|
}
|
|
164
|
+
if (
|
|
165
|
+
isCanonicalGjcWorkflowSkillFilesystemAlias(requestedName) &&
|
|
166
|
+
getSkillFilesystemIdentity(requestedName) !== requestedName.toLowerCase()
|
|
167
|
+
) {
|
|
168
|
+
throw new ToolError(`skill tool: refusing filesystem alias for bundled workflow skill "${requestedName}"`);
|
|
169
|
+
}
|
|
140
170
|
const activeState = this.#session.getActiveSkillState?.();
|
|
141
171
|
const activeSkill = normalizeSkillName(activeState?.skill);
|
|
142
172
|
if (activeSkill && requestedName === activeSkill) {
|
|
@@ -145,6 +175,9 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
|
|
|
145
175
|
);
|
|
146
176
|
}
|
|
147
177
|
|
|
178
|
+
const agentDir =
|
|
179
|
+
this.#session.getSessionAgentDir?.() ??
|
|
180
|
+
(this.#session.home === undefined ? this.#session.settings.getAgentDir() : undefined);
|
|
148
181
|
const skill =
|
|
149
182
|
skills.find(s => s.name === requestedName) ??
|
|
150
183
|
(await findRuntimeSkillByName(
|
|
@@ -152,12 +185,14 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
|
|
|
152
185
|
requestedName,
|
|
153
186
|
this.#getRuntimeSkillPolicy(),
|
|
154
187
|
this.#session.home,
|
|
188
|
+
agentDir,
|
|
189
|
+
this.#session.profileAuthority,
|
|
155
190
|
));
|
|
156
191
|
if (!skill) {
|
|
157
|
-
const available = skills
|
|
192
|
+
const available = formatAvailableSkills(skills);
|
|
158
193
|
const hint =
|
|
159
194
|
available.length > 0
|
|
160
|
-
? ` Available: ${available
|
|
195
|
+
? ` Available: ${available}. Use skill_discovery to find project/user runtime skills.`
|
|
161
196
|
: " Use skill_discovery to find project/user runtime skills.";
|
|
162
197
|
throw new ToolError(`skill tool: unknown skill "${requestedName}".${hint}`);
|
|
163
198
|
}
|
package/src/tools/subagent.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { ToolError } from "./tool-errors";
|
|
|
12
12
|
|
|
13
13
|
const DEFAULT_AWAIT_TIMEOUT_MS = 30_000;
|
|
14
14
|
const MAX_AWAIT_TIMEOUT_MS = 60 * 60 * 1000;
|
|
15
|
+
const MAX_AWAIT_HEARTBEAT_MS = 60_000;
|
|
15
16
|
/**
|
|
16
17
|
* Bounded cadence for await liveness: even when the rendered-state signature is
|
|
17
18
|
* stable (a running subagent emitting no new progress), the await panel re-emits
|
|
@@ -47,9 +48,9 @@ const subagentSchema = z.object({
|
|
|
47
48
|
.describe("terminal wait condition; defaults to all_terminal"),
|
|
48
49
|
heartbeat_ms: z
|
|
49
50
|
.number()
|
|
50
|
-
.refine(value => value === 0 || (Number.isInteger(value) && value >= 100 && value <=
|
|
51
|
+
.refine(value => value === 0 || (Number.isInteger(value) && value >= 100 && value <= MAX_AWAIT_HEARTBEAT_MS))
|
|
51
52
|
.optional()
|
|
52
|
-
.describe(
|
|
53
|
+
.describe(`heartbeat interval in milliseconds (100-${MAX_AWAIT_HEARTBEAT_MS}); 0 disables`),
|
|
53
54
|
timeout_ms: z.number().min(0).max(MAX_AWAIT_TIMEOUT_MS).optional().describe("await timeout in milliseconds"),
|
|
54
55
|
limit: z.number().min(1).max(MAX_LIST_LIMIT).optional().describe("maximum subagents to return"),
|
|
55
56
|
verbosity: z
|
|
@@ -227,7 +228,9 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
227
228
|
// or belong to a same-id subagent of another session (review thread P1).
|
|
228
229
|
const manager =
|
|
229
230
|
this.session.getAsyncJobManager?.() ??
|
|
230
|
-
AsyncJobManager.forEndpoint(
|
|
231
|
+
AsyncJobManager.forEndpoint(
|
|
232
|
+
this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
|
|
233
|
+
) ??
|
|
231
234
|
AsyncJobManager.instance();
|
|
232
235
|
if (!manager) {
|
|
233
236
|
return {
|
|
@@ -988,7 +988,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
|
|
|
988
988
|
"search": {
|
|
989
989
|
"name": "search",
|
|
990
990
|
"label": "Search",
|
|
991
|
-
"description": "Searches files using powerful regex matching.\n\n<instruction>\n- Supports Rust regex syntax (RE2-style — no lookaround or backreferences). Use line anchors or post-filters instead of (?!…)/(?<!…)\n- `paths` accepts an array of files, directories, globs, or internal URLs; when omitted, the whole working directory is searched\n- `paths` is an array; do not embed commas or spaces inside a single entry. Pass `[\"src\", \"tests\"]` not `[\"src,tests\"]`.\n- Cross-line patterns are detected from literal `\\n` or escaped `\\\\n` in `pattern`\n</instruction>\n\n<output>\n</output>\n\n<critical>\n- Search paths are an array; pass separate entries rather than comma-joined paths.\n- Use a cross-line pattern only when the match actually spans lines.\n</critical>",
|
|
991
|
+
"description": "Searches files using powerful regex matching.\n\n<instruction>\n- Supports Rust regex syntax (RE2-style — no lookaround or backreferences). Use line anchors or post-filters instead of (?!…)/(?<!…)\n- `paths` accepts an array of files, directories, globs, or internal URLs; when omitted, the whole working directory is searched\n- `paths` is an array; do not embed commas or spaces inside a single entry. Pass `[\"src\", \"tests\"]` not `[\"src,tests\"]`.\n- Cross-line patterns are detected from literal `\\n` or escaped `\\\\n` in `pattern`\n- `timeout` is in seconds (default 5, range 0.5–60). Increase it only for intentionally broad searches; narrow `paths` first\n</instruction>\n\n<output>\n</output>\n\n<critical>\n- Search paths are an array; pass separate entries rather than comma-joined paths.\n- Use a cross-line pattern only when the match actually spans lines.\n- Do not retry a timed-out search unchanged; narrow `paths`/`pattern` or explicitly increase `timeout`.\n</critical>",
|
|
992
992
|
"parameters": {
|
|
993
993
|
"type": "object",
|
|
994
994
|
"properties": {
|
|
@@ -1016,6 +1016,13 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
|
|
|
1016
1016
|
"skip": {
|
|
1017
1017
|
"description": "files to skip before collecting results — use to paginate when the prior call hit the file limit",
|
|
1018
1018
|
"type": "number"
|
|
1019
|
+
},
|
|
1020
|
+
"timeout": {
|
|
1021
|
+
"default": 5,
|
|
1022
|
+
"description": "timeout in seconds (0.5–60)",
|
|
1023
|
+
"type": "number",
|
|
1024
|
+
"minimum": 0.5,
|
|
1025
|
+
"maximum": 60
|
|
1019
1026
|
}
|
|
1020
1027
|
},
|
|
1021
1028
|
"required": [
|
|
@@ -2236,7 +2243,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
|
|
|
2236
2243
|
"subagent": {
|
|
2237
2244
|
"name": "subagent",
|
|
2238
2245
|
"label": "Subagent",
|
|
2239
|
-
"description": "Lists, inspects, awaits, pauses, resumes, steers, or cancels detached task subagents.\n\nTask launches return immediately. Use this tool when you need direct control over those running subagents. Prefer `subagent` for task subagents; generic `job` remains available for non-subagent jobs and compatibility fallback access.\n\n`verbosity` controls output size: `receipt` (default) returns status metadata plus a single ≤280-character result/error preview and an `agent://<id>` output ref when available; `preview` returns ≤2000 characters; `full` returns ≤12000 characters and requires explicit `ids`.\n\n# Operations\n\n## `action: \"list\"`\nSnapshot your visible detached subagents, including `running`, `paused`, `queued`, and terminal subagents when retained. Optional `limit` (1–50) caps how many subagents are returned. Output is receipt-only by default; use `verbosity: \"preview\"` for a bounded preview or inspect explicit `ids` with `verbosity: \"full\"` when fuller retained text is necessary.\n\n## `action: \"inspect\"`\nInspect selected subagents by `ids`; omit `ids` to inspect current running subagents. Terminal subagents return receipt-only output by default, with an `agent://<id>` ref when a verified output artifact is available. `verbosity: \"full\"` requires explicit `ids`.\n\n## `action: \"await\"`\nWait for selected subagents by `ids`; omit `ids` to wait for current running subagents.\n- Always set `timeout_ms` when the result is not immediately required forever.\n- Await timeout only bounds this tool call's wait; it does not stop the subagent and is not a failure reason.\n- On timeout, inspect progress and keep doing independent work. Never cancel just because an await timed out; cancel only if the subagent has actually failed, gone off-track, or become unrecoverably wrong.\n- Completed results are receipt-first by default: bounded preview plus `agent://<id>` output ref when available, not full retained output.\n\n## `action: \"pause\"`\nRequest a graceful safe-boundary pause for selected subagents by `ids`.\n- Non-running subagents are a no-op and return their current status snapshot.\n- A paused subagent keeps its session context and can be resumed later.\n\n## `action: \"resume\"`\nResume one subagent by `id` (preferred) or a single-item `ids` array.\n- Optional `message` is delivered into that one resumed run.\n- Running subagents are a no-op and return their current status snapshot.\n- Terminal subagents require `message` to start a follow-up resume run; without `message`, the tool returns the current snapshot with guidance.\n- `paused` subagents resume from saved context; `queued` subagents are already waiting for capacity.\n- Multiple targets are rejected because one global `message` must not broadcast to several subagents.\n\n## `action: \"steer\"`\nSend a non-empty `message` to one subagent by `id` (preferred) or a single-item `ids` array.\n- A running subagent receives the message through its live handle.\n- Optional `pause: true` requests a safe-boundary pause after steering a running subagent.\n- `pause` only matters while the target is running.\n- A non-active subagent (`paused`, `queued`, or terminal) automatically resumes with the message; `pause` is ignored for that target.\n- Multiple targets are rejected because one global `message` must not broadcast to several subagents.\n\n## `action: \"cancel\"`\nStop selected subagents by `ids`, including running, paused, or queued subagents.\n- Use only when the subagent has actually failed, gone off-track, or become unrecoverably wrong; an await timeout alone is never a cancellation reason.\n- Cancellation keeps the subagent session file for possible later context recovery.\n\n# Statuses\n\n- `running` — currently executing.\n- `paused` — stopped at a safe boundary with resumable context.\n- `queued` — resume requested and waiting for execution capacity.\n- `completed` — finished successfully.\n- `failed` — finished with an error.\n- `cancelled` — stopped by cancellation.\n- `not_found` — no visible subagent matches the requested id.",
|
|
2246
|
+
"description": "Lists, inspects, awaits, pauses, resumes, steers, or cancels detached task subagents.\n\nTask launches return immediately. Use this tool when you need direct control over those running subagents. Prefer `subagent` for task subagents; generic `job` remains available for non-subagent jobs and compatibility fallback access.\n\n`verbosity` controls output size: `receipt` (default) returns status metadata plus a single ≤280-character result/error preview and an `agent://<id>` output ref when available; `preview` returns ≤2000 characters; `full` returns ≤12000 characters and requires explicit `ids`.\n\n# Operations\n\n## `action: \"list\"`\nSnapshot your visible detached subagents, including `running`, `paused`, `queued`, and terminal subagents when retained. Optional `limit` (1–50) caps how many subagents are returned. Output is receipt-only by default; use `verbosity: \"preview\"` for a bounded preview or inspect explicit `ids` with `verbosity: \"full\"` when fuller retained text is necessary.\n\n## `action: \"inspect\"`\nInspect selected subagents by `ids`; omit `ids` to inspect current running subagents. Terminal subagents return receipt-only output by default, with an `agent://<id>` ref when a verified output artifact is available. `verbosity: \"full\"` requires explicit `ids`.\n\n## `action: \"await\"`\nWait for selected subagents by `ids`; omit `ids` to wait for current running subagents.\n- Always set `timeout_ms` when the result is not immediately required forever.\n- `heartbeat_ms` accepts integer intervals from 100 to 60,000 milliseconds (for example, 10,000 for ten seconds); `0` disables heartbeat updates and omission retains the 500 ms default. Terminal completion is event-driven, not delayed until the next heartbeat.\n- Await timeout only bounds this tool call's wait; it does not stop the subagent and is not a failure reason.\n- On timeout, inspect progress and keep doing independent work. Never cancel just because an await timed out; cancel only if the subagent has actually failed, gone off-track, or become unrecoverably wrong.\n- Completed results are receipt-first by default: bounded preview plus `agent://<id>` output ref when available, not full retained output.\n\n## `action: \"pause\"`\nRequest a graceful safe-boundary pause for selected subagents by `ids`.\n- Non-running subagents are a no-op and return their current status snapshot.\n- A paused subagent keeps its session context and can be resumed later.\n\n## `action: \"resume\"`\nResume one subagent by `id` (preferred) or a single-item `ids` array.\n- Optional `message` is delivered into that one resumed run.\n- Running subagents are a no-op and return their current status snapshot.\n- Terminal subagents require `message` to start a follow-up resume run; without `message`, the tool returns the current snapshot with guidance.\n- `paused` subagents resume from saved context; `queued` subagents are already waiting for capacity.\n- Multiple targets are rejected because one global `message` must not broadcast to several subagents.\n\n## `action: \"steer\"`\nSend a non-empty `message` to one subagent by `id` (preferred) or a single-item `ids` array.\n- A running subagent receives the message through its live handle.\n- Optional `pause: true` requests a safe-boundary pause after steering a running subagent.\n- `pause` only matters while the target is running.\n- A non-active subagent (`paused`, `queued`, or terminal) automatically resumes with the message; `pause` is ignored for that target.\n- Multiple targets are rejected because one global `message` must not broadcast to several subagents.\n\n## `action: \"cancel\"`\nStop selected subagents by `ids`, including running, paused, or queued subagents.\n- Use only when the subagent has actually failed, gone off-track, or become unrecoverably wrong; an await timeout alone is never a cancellation reason.\n- Cancellation keeps the subagent session file for possible later context recovery.\n\n# Statuses\n\n- `running` — currently executing.\n- `paused` — stopped at a safe boundary with resumable context.\n- `queued` — resume requested and waiting for execution capacity.\n- `completed` — finished successfully.\n- `failed` — finished with an error.\n- `cancelled` — stopped by cancellation.\n- `not_found` — no visible subagent matches the requested id.",
|
|
2240
2247
|
"parameters": {
|
|
2241
2248
|
"type": "object",
|
|
2242
2249
|
"properties": {
|
|
@@ -2281,7 +2288,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
|
|
|
2281
2288
|
]
|
|
2282
2289
|
},
|
|
2283
2290
|
"heartbeat_ms": {
|
|
2284
|
-
"description": "heartbeat interval; 0 disables",
|
|
2291
|
+
"description": "heartbeat interval in milliseconds (100-60000); 0 disables",
|
|
2285
2292
|
"type": "number"
|
|
2286
2293
|
},
|
|
2287
2294
|
"timeout_ms": {
|
|
@@ -70,9 +70,9 @@ export function guardToolForUltragoalAsk<T extends AgentTool>(
|
|
|
70
70
|
const candidate = tool as GuardedTool;
|
|
71
71
|
if (candidate[ULTRAGOAL_ASK_GUARD]) return tool;
|
|
72
72
|
const wrapped = new Proxy(tool, {
|
|
73
|
-
get(target, prop
|
|
73
|
+
get(target, prop) {
|
|
74
74
|
if (prop === ULTRAGOAL_ASK_GUARD) return true;
|
|
75
|
-
if (prop !== "execute") return Reflect.get(target, prop
|
|
75
|
+
if (prop !== "execute") return Reflect.get(target, prop);
|
|
76
76
|
return async (...args: unknown[]): Promise<unknown> => {
|
|
77
77
|
// The wrapper runs BEFORE AskTool.execute(): resolve the nudge
|
|
78
78
|
// budget against the SESSION profile, never the process-global one.
|
package/src/web/insane/bridge.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { type ChildProcess, spawn as nodeSpawn } from "node:child_process";
|
|
|
15
15
|
import * as path from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { $which } from "@gajae-code/utils";
|
|
18
|
+
import { probeLinuxProcPidSync } from "../../gjc-runtime/linux-proc";
|
|
19
|
+
import { groupLeaderIdentityMatches } from "../../runtime/process-lifecycle";
|
|
18
20
|
|
|
19
21
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
20
22
|
/** packages/coding-agent/vendor/insane-search */
|
|
@@ -119,22 +121,85 @@ function appendCapped(buffer: string, chunk: string, cap: number): string {
|
|
|
119
121
|
return buffer + (chunk.length > remaining ? chunk.slice(0, remaining) : chunk);
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Signal the child's whole process group on POSIX (the engine is spawned as a
|
|
126
|
+
* group leader, so its node/playwright grandchildren — including headless
|
|
127
|
+
* Chromium — are covered), with an exact-child fallback on Windows.
|
|
128
|
+
*/
|
|
129
|
+
type TeardownStatus = "terminated" | "still_running" | "identity_unverified";
|
|
130
|
+
|
|
131
|
+
function processGroupAlive(pgid: number): boolean {
|
|
124
132
|
try {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
process.kill(-pgid, 0);
|
|
134
|
+
return true;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function pollUntil(predicate: () => boolean, timeoutMs: number): Promise<boolean> {
|
|
141
|
+
if (predicate()) return true;
|
|
142
|
+
const deadline = Date.now() + timeoutMs;
|
|
143
|
+
while (Date.now() < deadline) {
|
|
144
|
+
await Bun.sleep(Math.min(20, deadline - Date.now()));
|
|
145
|
+
if (predicate()) return true;
|
|
128
146
|
}
|
|
129
|
-
|
|
147
|
+
return predicate();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Idempotently terminate the exact child/process group and await bounded reaping. */
|
|
151
|
+
function createChildTeardown(child: ChildProcess, childExited: Promise<void>): () => Promise<TeardownStatus> {
|
|
152
|
+
const pgid = process.platform !== "win32" ? child.pid : undefined;
|
|
153
|
+
const leader = pgid && process.platform === "linux" ? probeLinuxProcPidSync(pgid) : undefined;
|
|
154
|
+
const leaderStartTime = leader?.kind === "live" ? leader.startTime : undefined;
|
|
155
|
+
let teardownPromise: Promise<TeardownStatus> | undefined;
|
|
156
|
+
|
|
157
|
+
const identityMatches = (): boolean => {
|
|
158
|
+
if (!pgid || process.platform !== "linux") return true;
|
|
159
|
+
return groupLeaderIdentityMatches(leaderStartTime, probeLinuxProcPidSync(pgid));
|
|
160
|
+
};
|
|
161
|
+
const signalGroup = (signal: "SIGTERM" | "SIGKILL"): boolean => {
|
|
162
|
+
if (!pgid || !identityMatches()) return false;
|
|
130
163
|
try {
|
|
131
|
-
|
|
164
|
+
process.kill(-pgid, signal);
|
|
165
|
+
return true;
|
|
132
166
|
} catch {
|
|
133
|
-
|
|
167
|
+
return !processGroupAlive(pgid);
|
|
134
168
|
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
169
|
+
};
|
|
170
|
+
const signalChild = (signal: "SIGTERM" | "SIGKILL"): void => {
|
|
171
|
+
try {
|
|
172
|
+
child.kill(signal);
|
|
173
|
+
} catch {
|
|
174
|
+
// Exact child is already gone.
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return () => {
|
|
179
|
+
if (teardownPromise) return teardownPromise;
|
|
180
|
+
teardownPromise = (async (): Promise<TeardownStatus> => {
|
|
181
|
+
if (pgid) {
|
|
182
|
+
if (!processGroupAlive(pgid)) return "terminated";
|
|
183
|
+
if (!identityMatches()) return "identity_unverified";
|
|
184
|
+
signalGroup("SIGTERM");
|
|
185
|
+
if (await pollUntil(() => !processGroupAlive(pgid), KILL_GRACE_MS)) return "terminated";
|
|
186
|
+
if (!identityMatches()) return "identity_unverified";
|
|
187
|
+
signalGroup("SIGKILL");
|
|
188
|
+
return (await pollUntil(() => !processGroupAlive(pgid), KILL_GRACE_MS)) ? "terminated" : "still_running";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (child.exitCode !== null || child.signalCode !== null) return "terminated";
|
|
192
|
+
signalChild("SIGTERM");
|
|
193
|
+
if ((await Promise.race([childExited.then(() => true), Bun.sleep(KILL_GRACE_MS).then(() => false)])) === true)
|
|
194
|
+
return "terminated";
|
|
195
|
+
signalChild("SIGKILL");
|
|
196
|
+
return (await Promise.race([childExited.then(() => true), Bun.sleep(KILL_GRACE_MS).then(() => false)])) ===
|
|
197
|
+
true
|
|
198
|
+
? "terminated"
|
|
199
|
+
: "still_running";
|
|
200
|
+
})();
|
|
201
|
+
return teardownPromise;
|
|
202
|
+
};
|
|
138
203
|
}
|
|
139
204
|
|
|
140
205
|
/** Real engine runner: `python3 -m engine "<url>" --json`. */
|
|
@@ -149,31 +214,45 @@ export function runEngineSubprocess(
|
|
|
149
214
|
let settled = false;
|
|
150
215
|
let timedOut = false;
|
|
151
216
|
let aborted = false;
|
|
217
|
+
let closeObserved = false;
|
|
218
|
+
let closeCode: number | null = null;
|
|
219
|
+
let teardownDone = false;
|
|
220
|
+
let teardownPromise: Promise<TeardownStatus> | undefined;
|
|
221
|
+
const { promise: childExited, resolve: resolveChildExited } = Promise.withResolvers<void>();
|
|
152
222
|
|
|
153
223
|
const child = spawnImpl("python3", ["-m", "engine", inv.url, "--json"], {
|
|
154
224
|
cwd: INSANE_VENDOR_DIR,
|
|
155
225
|
env: { ...process.env, PYTHONPATH: INSANE_VENDOR_DIR },
|
|
156
226
|
stdio: ["ignore", "pipe", "pipe"],
|
|
227
|
+
// Own process group on POSIX so killChild can signal the engine's
|
|
228
|
+
// whole tree (python3 -> node -> headless Chromium) in one shot.
|
|
229
|
+
detached: process.platform !== "win32",
|
|
157
230
|
});
|
|
231
|
+
const teardown = createChildTeardown(child, childExited);
|
|
158
232
|
|
|
159
|
-
const finish = (
|
|
233
|
+
const finish = (): void => {
|
|
160
234
|
if (settled) return;
|
|
235
|
+
if (teardownPromise ? !teardownDone : !closeObserved) return;
|
|
161
236
|
settled = true;
|
|
162
237
|
clearTimeout(timer);
|
|
163
238
|
inv.signal?.removeEventListener("abort", onAbort);
|
|
164
|
-
resolve({ code, stdout, stderr, timedOut, aborted });
|
|
239
|
+
resolve({ code: closeObserved ? closeCode : null, stdout, stderr, timedOut, aborted });
|
|
240
|
+
};
|
|
241
|
+
const requestStop = (reason: "timeout" | "abort"): void => {
|
|
242
|
+
if (teardownPromise) return;
|
|
243
|
+
if (reason === "timeout") timedOut = true;
|
|
244
|
+
else aborted = true;
|
|
245
|
+
teardownPromise = teardown();
|
|
246
|
+
void teardownPromise.finally(() => {
|
|
247
|
+
teardownDone = true;
|
|
248
|
+
finish();
|
|
249
|
+
});
|
|
165
250
|
};
|
|
166
251
|
|
|
167
|
-
const timer = setTimeout(() =>
|
|
168
|
-
timedOut = true;
|
|
169
|
-
killChild(child);
|
|
170
|
-
}, inv.timeoutMs);
|
|
252
|
+
const timer = setTimeout(() => requestStop("timeout"), inv.timeoutMs);
|
|
171
253
|
timer.unref?.();
|
|
172
254
|
|
|
173
|
-
const onAbort = (): void =>
|
|
174
|
-
aborted = true;
|
|
175
|
-
killChild(child);
|
|
176
|
-
};
|
|
255
|
+
const onAbort = (): void => requestStop("abort");
|
|
177
256
|
if (inv.signal) {
|
|
178
257
|
if (inv.signal.aborted) onAbort();
|
|
179
258
|
else inv.signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -185,8 +264,19 @@ export function runEngineSubprocess(
|
|
|
185
264
|
child.stderr?.on("data", (chunk: Buffer) => {
|
|
186
265
|
stderr = appendCapped(stderr, chunk.toString("utf8"), MAX_STDERR_BYTES);
|
|
187
266
|
});
|
|
188
|
-
child.
|
|
189
|
-
child.on("
|
|
267
|
+
child.once("exit", () => resolveChildExited());
|
|
268
|
+
child.on("error", () => {
|
|
269
|
+
resolveChildExited();
|
|
270
|
+
closeObserved = true;
|
|
271
|
+
closeCode = null;
|
|
272
|
+
finish();
|
|
273
|
+
});
|
|
274
|
+
child.on("close", code => {
|
|
275
|
+
resolveChildExited();
|
|
276
|
+
closeObserved = true;
|
|
277
|
+
closeCode = code;
|
|
278
|
+
finish();
|
|
279
|
+
});
|
|
190
280
|
});
|
|
191
281
|
}
|
|
192
282
|
|
|
@@ -9,16 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
import { type AuthStorage, getEnvApiKey } from "@gajae-code/ai/core";
|
|
11
11
|
import { $env, readSseJson } from "@gajae-code/utils";
|
|
12
|
-
import type {
|
|
13
|
-
PerplexityMessageOutput,
|
|
14
|
-
PerplexityRequest,
|
|
15
|
-
PerplexityResponse,
|
|
16
|
-
SearchCitation,
|
|
17
|
-
SearchResponse,
|
|
18
|
-
SearchSource,
|
|
19
|
-
} from "../../../web/search/types";
|
|
12
|
+
import type { PerplexityRequest, SearchCitation, SearchResponse, SearchSource } from "../../../web/search/types";
|
|
20
13
|
import { SearchProviderError } from "../../../web/search/types";
|
|
21
|
-
import { dateToAgeSeconds } from "../utils";
|
|
14
|
+
import { clampNumResults, dateToAgeSeconds } from "../utils";
|
|
22
15
|
import type { SearchParams } from "./base";
|
|
23
16
|
import { SearchProvider } from "./base";
|
|
24
17
|
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
@@ -29,6 +22,8 @@ const PERPLEXITY_OAUTH_ASK_URL = "https://www.perplexity.ai/rest/sse/perplexity_
|
|
|
29
22
|
const DEFAULT_MAX_TOKENS = 8192;
|
|
30
23
|
const DEFAULT_TEMPERATURE = 0.2;
|
|
31
24
|
const DEFAULT_NUM_SEARCH_RESULTS = 10;
|
|
25
|
+
const MIN_NUM_SEARCH_RESULTS = 3;
|
|
26
|
+
const MAX_NUM_SEARCH_RESULTS = 100;
|
|
32
27
|
const OAUTH_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
|
33
28
|
const OAUTH_API_VERSION = "2.18";
|
|
34
29
|
const OAUTH_USER_AGENT = "Perplexity/641 CFNetwork/1568 Darwin/25.2.0";
|
|
@@ -239,11 +234,7 @@ async function findPerplexityAuth(
|
|
|
239
234
|
}
|
|
240
235
|
|
|
241
236
|
/** Call Perplexity API-key endpoint. */
|
|
242
|
-
async function callPerplexityApi(
|
|
243
|
-
apiKey: string,
|
|
244
|
-
request: PerplexityRequest,
|
|
245
|
-
signal?: AbortSignal,
|
|
246
|
-
): Promise<PerplexityResponse> {
|
|
237
|
+
async function callPerplexityApi(apiKey: string, request: PerplexityRequest, signal?: AbortSignal): Promise<unknown> {
|
|
247
238
|
const response = await fetch(PERPLEXITY_API_URL, {
|
|
248
239
|
method: "POST",
|
|
249
240
|
headers: {
|
|
@@ -258,14 +249,15 @@ async function callPerplexityApi(
|
|
|
258
249
|
const errorText = await response.text();
|
|
259
250
|
const classified = classifyProviderHttpError("perplexity", response.status, errorText);
|
|
260
251
|
if (classified) throw classified;
|
|
261
|
-
throw new SearchProviderError(
|
|
262
|
-
"perplexity",
|
|
263
|
-
`Perplexity API error (${response.status}): ${errorText}`,
|
|
264
|
-
response.status,
|
|
265
|
-
);
|
|
252
|
+
throw new SearchProviderError("perplexity", `Perplexity API error (${response.status})`, response.status);
|
|
266
253
|
}
|
|
267
254
|
|
|
268
|
-
|
|
255
|
+
try {
|
|
256
|
+
return await response.json();
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (error instanceof Error && error.name === "AbortError") throw error;
|
|
259
|
+
throw new SearchProviderError("perplexity", "Perplexity API returned invalid JSON", 502);
|
|
260
|
+
}
|
|
269
261
|
}
|
|
270
262
|
|
|
271
263
|
function buildOAuthSources(event: PerplexityOAuthStreamEvent): SearchSource[] {
|
|
@@ -377,11 +369,7 @@ async function callPerplexityOAuth(
|
|
|
377
369
|
const errorText = await response.text();
|
|
378
370
|
const classified = classifyProviderHttpError("perplexity", response.status, errorText);
|
|
379
371
|
if (classified) throw classified;
|
|
380
|
-
throw new SearchProviderError(
|
|
381
|
-
"perplexity",
|
|
382
|
-
`Perplexity OAuth API error (${response.status}): ${errorText}`,
|
|
383
|
-
response.status,
|
|
384
|
-
);
|
|
372
|
+
throw new SearchProviderError("perplexity", `Perplexity OAuth API error (${response.status})`, response.status);
|
|
385
373
|
}
|
|
386
374
|
|
|
387
375
|
if (!response.body) {
|
|
@@ -426,22 +414,79 @@ async function callPerplexityOAuth(
|
|
|
426
414
|
};
|
|
427
415
|
}
|
|
428
416
|
|
|
429
|
-
|
|
430
|
-
|
|
417
|
+
type JsonObject = Record<string, unknown>;
|
|
418
|
+
|
|
419
|
+
function malformedResponse(detail: string): never {
|
|
420
|
+
throw new SearchProviderError("perplexity", `Perplexity API returned malformed response body (${detail})`, 502);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function isJsonObject(value: unknown): value is JsonObject {
|
|
424
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function optionalArray(value: unknown, detail: string): unknown[] {
|
|
428
|
+
if (value === undefined || value === null) return [];
|
|
429
|
+
if (!Array.isArray(value)) malformedResponse(detail);
|
|
430
|
+
return value;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function messageContentToText(content: unknown): string {
|
|
434
|
+
if (content === undefined || content === null) return "";
|
|
431
435
|
if (typeof content === "string") return content;
|
|
432
|
-
|
|
436
|
+
if (!Array.isArray(content)) malformedResponse("message content must be a string or array");
|
|
437
|
+
|
|
438
|
+
return content
|
|
439
|
+
.map(chunk => {
|
|
440
|
+
if (!isJsonObject(chunk) || typeof chunk.type !== "string") {
|
|
441
|
+
malformedResponse("message content chunks must be objects with a type");
|
|
442
|
+
}
|
|
443
|
+
if (chunk.type !== "text") return "";
|
|
444
|
+
if (typeof chunk.text !== "string") malformedResponse("text content chunks must contain text");
|
|
445
|
+
return chunk.text;
|
|
446
|
+
})
|
|
447
|
+
.join("");
|
|
433
448
|
}
|
|
434
449
|
|
|
435
450
|
/** Parse API response into unified SearchResponse */
|
|
436
|
-
function parseResponse(
|
|
437
|
-
|
|
451
|
+
function parseResponse(value: unknown): SearchResponse {
|
|
452
|
+
if (!isJsonObject(value)) malformedResponse("expected a JSON object");
|
|
453
|
+
|
|
454
|
+
const choices = optionalArray(value.choices, "choices must be an array");
|
|
455
|
+
const firstChoice = choices[0];
|
|
456
|
+
if (firstChoice !== undefined && !isJsonObject(firstChoice)) malformedResponse("choices must contain objects");
|
|
457
|
+
const message = firstChoice?.message;
|
|
458
|
+
if (message !== undefined && !isJsonObject(message)) malformedResponse("choice message must be an object");
|
|
459
|
+
const messageContent = message?.content;
|
|
438
460
|
const answer = messageContentToText(messageContent);
|
|
439
461
|
|
|
440
462
|
const sources: SearchSource[] = [];
|
|
441
463
|
const citations: SearchCitation[] = [];
|
|
442
464
|
|
|
443
|
-
const citationUrls =
|
|
444
|
-
|
|
465
|
+
const citationUrls = optionalArray(value.citations, "citations must be an array").map(url => {
|
|
466
|
+
if (typeof url !== "string" || url.length === 0) malformedResponse("citations must contain non-empty URLs");
|
|
467
|
+
return url;
|
|
468
|
+
});
|
|
469
|
+
const searchResults = optionalArray(value.search_results, "search_results must be an array").map(result => {
|
|
470
|
+
if (!isJsonObject(result)) malformedResponse("search_results must contain objects");
|
|
471
|
+
if (typeof result.url !== "string" || result.url.length === 0) {
|
|
472
|
+
malformedResponse("search_results entries must contain a non-empty URL");
|
|
473
|
+
}
|
|
474
|
+
if (result.title !== undefined && typeof result.title !== "string") {
|
|
475
|
+
malformedResponse("search_results titles must be strings");
|
|
476
|
+
}
|
|
477
|
+
if (result.snippet !== undefined && typeof result.snippet !== "string") {
|
|
478
|
+
malformedResponse("search_results snippets must be strings");
|
|
479
|
+
}
|
|
480
|
+
if (result.date !== undefined && result.date !== null && typeof result.date !== "string") {
|
|
481
|
+
malformedResponse("search_results dates must be strings");
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
title: result.title ?? result.url,
|
|
485
|
+
url: result.url,
|
|
486
|
+
snippet: result.snippet,
|
|
487
|
+
date: result.date,
|
|
488
|
+
};
|
|
489
|
+
});
|
|
445
490
|
|
|
446
491
|
if (citationUrls.length > 0) {
|
|
447
492
|
for (const url of citationUrls) {
|
|
@@ -475,18 +520,23 @@ function parseResponse(response: PerplexityResponse): SearchResponse {
|
|
|
475
520
|
answer: answer || undefined,
|
|
476
521
|
sources,
|
|
477
522
|
citations: citations.length > 0 ? citations : undefined,
|
|
478
|
-
usage:
|
|
523
|
+
usage: isJsonObject(value.usage)
|
|
479
524
|
? {
|
|
480
|
-
inputTokens:
|
|
481
|
-
outputTokens:
|
|
482
|
-
|
|
525
|
+
inputTokens: typeof value.usage.prompt_tokens === "number" ? value.usage.prompt_tokens : undefined,
|
|
526
|
+
outputTokens:
|
|
527
|
+
typeof value.usage.completion_tokens === "number" ? value.usage.completion_tokens : undefined,
|
|
528
|
+
totalTokens: typeof value.usage.total_tokens === "number" ? value.usage.total_tokens : undefined,
|
|
483
529
|
}
|
|
484
530
|
: undefined,
|
|
485
|
-
model:
|
|
486
|
-
requestId:
|
|
531
|
+
model: typeof value.model === "string" ? value.model : undefined,
|
|
532
|
+
requestId: typeof value.id === "string" ? value.id : undefined,
|
|
487
533
|
};
|
|
488
534
|
}
|
|
489
535
|
|
|
536
|
+
function normalizeNumSearchResults(value: number | undefined): number {
|
|
537
|
+
return Math.max(MIN_NUM_SEARCH_RESULTS, clampNumResults(value, DEFAULT_NUM_SEARCH_RESULTS, MAX_NUM_SEARCH_RESULTS));
|
|
538
|
+
}
|
|
539
|
+
|
|
490
540
|
function applySourceLimit(result: SearchResponse, limit?: number): SearchResponse {
|
|
491
541
|
if (limit && result.sources.length > limit) {
|
|
492
542
|
result.sources = result.sources.slice(0, limit);
|
|
@@ -503,6 +553,9 @@ export async function searchPerplexity(params: PerplexitySearchParams): Promise<
|
|
|
503
553
|
|
|
504
554
|
if (auth.type === "oauth" || auth.type === "cookies") {
|
|
505
555
|
const oauthResult = await callPerplexityOAuth(auth, params);
|
|
556
|
+
if (!oauthResult.answer.trim()) {
|
|
557
|
+
throw new SearchProviderError("perplexity", "Perplexity web search returned no answer", 424);
|
|
558
|
+
}
|
|
506
559
|
return applySourceLimit(
|
|
507
560
|
{
|
|
508
561
|
provider: "perplexity",
|
|
@@ -529,7 +582,7 @@ export async function searchPerplexity(params: PerplexitySearchParams): Promise<
|
|
|
529
582
|
max_tokens: params.max_tokens ?? DEFAULT_MAX_TOKENS,
|
|
530
583
|
temperature: params.temperature ?? DEFAULT_TEMPERATURE,
|
|
531
584
|
search_mode: "web",
|
|
532
|
-
num_search_results: params.num_search_results
|
|
585
|
+
num_search_results: normalizeNumSearchResults(params.num_search_results),
|
|
533
586
|
web_search_options: {
|
|
534
587
|
search_type: "pro",
|
|
535
588
|
search_context_size: "medium",
|
|
@@ -543,8 +596,16 @@ export async function searchPerplexity(params: PerplexitySearchParams): Promise<
|
|
|
543
596
|
request.search_recency_filter = params.search_recency_filter;
|
|
544
597
|
}
|
|
545
598
|
|
|
546
|
-
|
|
547
|
-
|
|
599
|
+
let result: SearchResponse | undefined;
|
|
600
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
601
|
+
const response = await callPerplexityApi(auth.token, request, params.signal);
|
|
602
|
+
result = parseResponse(response);
|
|
603
|
+
if (result.answer?.trim()) break;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!result?.answer?.trim()) {
|
|
607
|
+
throw new SearchProviderError("perplexity", "Perplexity web search returned no answer", 424);
|
|
608
|
+
}
|
|
548
609
|
result.authMode = "api_key";
|
|
549
610
|
return applySourceLimit(result, params.num_results);
|
|
550
611
|
}
|