@selesai/code 0.1.19 → 0.2.1
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/dist/agents/architect.md +4 -3
- package/dist/agents/builder.md +5 -4
- package/dist/agents/commentator.md +3 -2
- package/dist/agents/explorer.md +4 -4
- package/dist/agents/recapper.md +4 -3
- package/dist/config.d.ts +6 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -9
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +3 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +18 -24
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +2 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +8 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +25 -11
- package/dist/core/skills.js.map +1 -1
- package/dist/defaults/models.json +143 -39
- package/dist/defaults/settings.json +2 -29
- package/dist/extensions/package.json +5 -4
- package/dist/extensions/pi-intercom/index.ts +12 -5
- package/dist/extensions/pi-powerline-footer/index.ts +4 -1
- package/dist/extensions/pi-powerline-footer/tps.ts +286 -0
- package/dist/extensions/pi-powerline-footer/welcome.ts +47 -46
- package/dist/extensions/pi-subagents/README.md +12 -5
- package/dist/extensions/pi-subagents/agents/context-builder.md +2 -2
- package/dist/extensions/pi-subagents/agents/researcher.md +4 -5
- package/dist/extensions/pi-subagents/package.json +8 -5
- package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
- package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +1 -1
- package/dist/extensions/pi-subagents/src/agents/agents.ts +3 -3
- package/dist/extensions/pi-subagents/src/extension/control-notices.ts +1 -1
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +1 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +29 -2
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +72 -13
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +85 -41
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +46 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +1 -3
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +18 -18
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +24 -24
- package/dist/extensions/pi-subagents/src/runs/shared/spawn-timing.ts +46 -0
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -4
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +1 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +6 -6
- package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -3
- package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -1
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
- package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +1 -1
- package/dist/extensions/pi-subagents/src/tui/render.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/spawn-timing.test.ts +45 -0
- package/dist/extensions/pi-subagents/tsconfig.runner.json +17 -0
- package/dist/extensions/pi-web-agent/package.json +31 -0
- package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -0
- package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -0
- package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -0
- package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -0
- package/dist/extensions/pi-web-agent/src/changelog-notice.ts +133 -0
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -0
- package/dist/extensions/pi-web-agent/src/extension.ts +126 -0
- package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -0
- package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -0
- package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -0
- package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -0
- package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -0
- package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -0
- package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -0
- package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -0
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -0
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -0
- package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -0
- package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -0
- package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -0
- package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -0
- package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +208 -0
- package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -0
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -0
- package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -0
- package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -0
- package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -0
- package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -0
- package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -0
- package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -0
- package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -0
- package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -0
- package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -0
- package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -0
- package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -0
- package/dist/extensions/pi-web-agent/src/types.ts +88 -0
- package/dist/extensions/ponytail/index.js +189 -0
- package/dist/extensions/ponytail/package.json +8 -0
- package/dist/extensions/ponytail/ponytail-config.cjs +122 -0
- package/dist/extensions/ponytail/ponytail-instructions.cjs +94 -0
- package/dist/extensions/ponytail/test/extension.test.js +167 -0
- package/dist/extensions/ponytail/test/helpers.test.js +92 -0
- package/dist/extensions/question/constants.ts +38 -0
- package/dist/extensions/question/dialog-adapter.ts +74 -0
- package/dist/extensions/question/helpers.ts +102 -0
- package/dist/extensions/question/index.ts +671 -0
- package/dist/extensions/question/navigation.ts +14 -0
- package/dist/extensions/question/package.json +18 -0
- package/dist/extensions/question/question-list.ts +498 -0
- package/dist/extensions/question/row-layout.ts +162 -0
- package/dist/extensions/question/schemas.ts +69 -0
- package/dist/extensions/question/selection-mode.ts +49 -0
- package/dist/extensions/question/shortcuts.ts +53 -0
- package/dist/extensions/question/tests/helpers.test.ts +178 -0
- package/dist/extensions/question/tests/question-list.test.ts +230 -0
- package/dist/extensions/question/tests/row-layout.test.ts +136 -0
- package/dist/extensions/question/tests/shortcuts.test.ts +85 -0
- package/dist/extensions/question/tests/ui-protocol.test.ts +143 -0
- package/dist/extensions/question/tui-adapter.ts +33 -0
- package/dist/extensions/question/types.ts +102 -0
- package/dist/extensions/question/ui-protocol.ts +46 -0
- package/dist/extensions/tokenin-onboarding.ts +265 -2
- package/dist/extensions/web-agent-onboarding.ts +222 -0
- package/dist/extensions/workflow/adapter.ts +588 -0
- package/dist/extensions/workflow/extension.ts +24 -0
- package/dist/extensions/workflow/modes/prototype.ts +156 -0
- package/dist/extensions/workflow/modes/quick.ts +138 -0
- package/dist/extensions/workflow/package.json +17 -0
- package/dist/extensions/workflow/state-machine.ts +386 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +57 -13
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/mime.d.ts.map +1 -1
- package/dist/utils/mime.js +24 -1
- package/dist/utils/mime.js.map +1 -1
- package/docs/docs.json +4 -0
- package/docs/workflows.md +206 -0
- package/package.json +5 -1
- package/README.md +0 -198
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { ResearchSourceKind } from './research-types.js';
|
|
2
|
+
|
|
3
|
+
export type SourceProfileKind =
|
|
4
|
+
| 'official-docs'
|
|
5
|
+
| 'official-api'
|
|
6
|
+
| 'issue-thread'
|
|
7
|
+
| 'forum-thread'
|
|
8
|
+
| 'package-page'
|
|
9
|
+
| 'community';
|
|
10
|
+
|
|
11
|
+
export type SourceProfile = {
|
|
12
|
+
kind: SourceProfileKind;
|
|
13
|
+
sourceKind: ResearchSourceKind;
|
|
14
|
+
shouldPreferHeadlessWhenWeak: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const COMMUNITY_FORUM_HOST_RE = /(^|\.)(community|forum|forums|discuss|discourse)\./;
|
|
18
|
+
|
|
19
|
+
function profile(
|
|
20
|
+
kind: SourceProfileKind,
|
|
21
|
+
sourceKind: ResearchSourceKind,
|
|
22
|
+
shouldPreferHeadlessWhenWeak: boolean
|
|
23
|
+
): SourceProfile {
|
|
24
|
+
return { kind, sourceKind, shouldPreferHeadlessWhenWeak };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseUrl(rawUrl: string): URL | undefined {
|
|
28
|
+
try {
|
|
29
|
+
return new URL(rawUrl);
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isOfficialApi(host: string, path: string) {
|
|
36
|
+
return (
|
|
37
|
+
(host === 'playwright.dev' && path.startsWith('/docs/api/')) ||
|
|
38
|
+
(host === 'vitest.dev' && path.startsWith('/config/'))
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isOfficialDocs(host: string, path: string) {
|
|
43
|
+
return (
|
|
44
|
+
(host === 'playwright.dev' && path.startsWith('/docs/')) ||
|
|
45
|
+
(host === 'vitest.dev' && path.startsWith('/guide/')) ||
|
|
46
|
+
(host === 'github.com' && path.startsWith('/vitest-dev/vitest/') && path.includes('/docs/')) ||
|
|
47
|
+
host === 'learn.microsoft.com'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isIssueThread(host: string, path: string) {
|
|
52
|
+
return host === 'github.com' && (path.includes('/issues/') || path.includes('/discussions/'));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hasForumThreadPath(path: string) {
|
|
56
|
+
return (
|
|
57
|
+
path.includes('/forum/') ||
|
|
58
|
+
path.includes('/forums/') ||
|
|
59
|
+
path.includes('/t/') ||
|
|
60
|
+
path.includes('/topic/') ||
|
|
61
|
+
path.includes('/threads/')
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isForumThread(host: string, path: string) {
|
|
66
|
+
return (
|
|
67
|
+
(host === 'reddit.com' && path.includes('/comments/')) ||
|
|
68
|
+
(host === 'stackoverflow.com' && path.startsWith('/questions/')) ||
|
|
69
|
+
(COMMUNITY_FORUM_HOST_RE.test(`${host}.`) && hasForumThreadPath(path))
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function classifySourceProfile(rawUrl: string): SourceProfile {
|
|
74
|
+
const parsed = parseUrl(rawUrl);
|
|
75
|
+
if (!parsed) return profile('community', 'community', false);
|
|
76
|
+
|
|
77
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, '');
|
|
78
|
+
const path = parsed.pathname.toLowerCase();
|
|
79
|
+
|
|
80
|
+
if (isOfficialApi(host, path)) {
|
|
81
|
+
return profile('official-api', 'official-api', false);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (isOfficialDocs(host, path)) {
|
|
85
|
+
return profile('official-docs', 'official-docs', false);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isIssueThread(host, path)) {
|
|
89
|
+
return profile('issue-thread', 'issue-thread', true);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (isForumThread(host, path)) {
|
|
93
|
+
return profile('forum-thread', 'community', true);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (host === 'npmjs.com' && path.startsWith('/package/')) {
|
|
97
|
+
return profile('package-page', 'package-page', false);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return profile('community', 'community', false);
|
|
101
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ResearchEvidence } from './research-types.js';
|
|
2
|
+
import type { EvidenceQualityReport } from './evidence-quality.js';
|
|
3
|
+
import { hasOfficialEvidence, strongEvidenceCount } from './evidence-ranker.js';
|
|
4
|
+
|
|
5
|
+
export type ResearchStepDecision =
|
|
6
|
+
| { action: 'answer' }
|
|
7
|
+
| { action: 'answer-with-caveat' }
|
|
8
|
+
| { action: 'search-again' }
|
|
9
|
+
| { action: 'headless'; url: string };
|
|
10
|
+
|
|
11
|
+
function activeCaveatReasons(evidence: ResearchEvidence[], quality?: EvidenceQualityReport) {
|
|
12
|
+
const reasons = quality?.caveatReasons ?? [];
|
|
13
|
+
if (!hasOfficialDocsAndApi(evidence)) return reasons;
|
|
14
|
+
return reasons.filter((reason) => reason !== 'low-diversity');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hasQualityConcern(evidence: ResearchEvidence[], quality?: EvidenceQualityReport) {
|
|
18
|
+
return activeCaveatReasons(evidence, quality).length > 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasOfficialDocsAndApi(evidence: ResearchEvidence[]) {
|
|
22
|
+
return evidence.some((item) => item.sourceKind === 'official-docs') &&
|
|
23
|
+
evidence.some((item) => item.sourceKind === 'official-api');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function shouldSearchForBetterQuality({
|
|
27
|
+
evidence,
|
|
28
|
+
quality,
|
|
29
|
+
passIndex,
|
|
30
|
+
maxPasses
|
|
31
|
+
}: {
|
|
32
|
+
evidence: ResearchEvidence[];
|
|
33
|
+
quality?: EvidenceQualityReport;
|
|
34
|
+
passIndex: number;
|
|
35
|
+
maxPasses: number;
|
|
36
|
+
}) {
|
|
37
|
+
if (!quality) return false;
|
|
38
|
+
if (passIndex + 1 >= maxPasses) return false;
|
|
39
|
+
if (quality.flags.hasOnlyCommunityEvidence) return true;
|
|
40
|
+
if (quality.flags.hasLowDiversity && !hasOfficialDocsAndApi(evidence)) return true;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function decideNextResearchStep({
|
|
45
|
+
evidence,
|
|
46
|
+
suggestedHeadlessUrls,
|
|
47
|
+
passIndex,
|
|
48
|
+
maxPasses,
|
|
49
|
+
headlessAttempts,
|
|
50
|
+
maxHeadlessAttempts,
|
|
51
|
+
quality
|
|
52
|
+
}: {
|
|
53
|
+
evidence: ResearchEvidence[];
|
|
54
|
+
suggestedHeadlessUrls: string[];
|
|
55
|
+
passIndex: number;
|
|
56
|
+
maxPasses: number;
|
|
57
|
+
headlessAttempts: number;
|
|
58
|
+
maxHeadlessAttempts: number;
|
|
59
|
+
quality?: EvidenceQualityReport;
|
|
60
|
+
}): ResearchStepDecision {
|
|
61
|
+
const strongEnough = strongEvidenceCount(evidence) >= 2 && hasOfficialEvidence(evidence);
|
|
62
|
+
|
|
63
|
+
if (strongEnough && shouldSearchForBetterQuality({ evidence, quality, passIndex, maxPasses })) {
|
|
64
|
+
return { action: 'search-again' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (strongEnough) {
|
|
68
|
+
return hasQualityConcern(evidence, quality) ? { action: 'answer-with-caveat' } : { action: 'answer' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const headlessUrl = suggestedHeadlessUrls.find((url) => !url.includes('npmjs.com/package/'));
|
|
72
|
+
if (headlessUrl && headlessAttempts < maxHeadlessAttempts) {
|
|
73
|
+
return { action: 'headless', url: headlessUrl };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (passIndex + 1 < maxPasses) {
|
|
77
|
+
return { action: 'search-again' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { action: 'answer-with-caveat' };
|
|
81
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_BACKEND_CONFIG,
|
|
6
|
+
extractBackendConfigOverride,
|
|
7
|
+
mergeBackendConfigLayers,
|
|
8
|
+
type BackendConfig,
|
|
9
|
+
type BackendConfigFile,
|
|
10
|
+
type BackendConfigOverride
|
|
11
|
+
} from '../backends/config.js';
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_PRESENTATION_CONFIG,
|
|
14
|
+
extractPresentationConfigOverride,
|
|
15
|
+
mergePresentationConfigLayers
|
|
16
|
+
} from './config.js';
|
|
17
|
+
import type {
|
|
18
|
+
PresentationConfig,
|
|
19
|
+
PresentationConfigFile,
|
|
20
|
+
PresentationConfigOverride,
|
|
21
|
+
PresentationScope
|
|
22
|
+
} from './types.js';
|
|
23
|
+
|
|
24
|
+
export type PresentationConfigStoreOptions = {
|
|
25
|
+
homeDir?: string;
|
|
26
|
+
projectDir?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type PresentationConfigLayer = {
|
|
30
|
+
path: string;
|
|
31
|
+
exists: boolean;
|
|
32
|
+
rawConfig?: PresentationConfigOverride;
|
|
33
|
+
rawBackends?: BackendConfigOverride;
|
|
34
|
+
error?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type LoadedPresentationConfig = {
|
|
38
|
+
global: PresentationConfigLayer;
|
|
39
|
+
project: PresentationConfigLayer;
|
|
40
|
+
effectiveConfig: PresentationConfig;
|
|
41
|
+
effectiveBackends: BackendConfig;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function getPresentationConfigPaths(options: PresentationConfigStoreOptions = {}) {
|
|
45
|
+
// ponytail: use selesai's getAgentDir() so the config dir respects the active
|
|
46
|
+
// CONFIG_DIR_NAME (e.g. .selesai) instead of upstream's hardcoded .pi. Ceiling:
|
|
47
|
+
// if selesai changes the extensions config layout, update here; upgrade path =
|
|
48
|
+
// upstream pi-web-agent accepting a config-dir resolver.
|
|
49
|
+
const homeDir = options.homeDir ?? process.env.USERPROFILE ?? process.env.HOME ?? '';
|
|
50
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
51
|
+
const agentDir = getAgentDir();
|
|
52
|
+
// agentDir is already <homeDir>/<CONFIG_DIR_NAME>/agent; derive the home-relative config dir.
|
|
53
|
+
const globalConfigRoot = path.join(agentDir, 'extensions', 'pi-web-agent', 'config.json');
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
globalPath: globalConfigRoot,
|
|
57
|
+
projectPath: path.join(projectDir, '.pi', 'extensions', 'pi-web-agent', 'config.json')
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type AgentConfigFile = PresentationConfigFile & BackendConfigFile;
|
|
62
|
+
|
|
63
|
+
type LegacyPresentationConfigFile = NonNullable<PresentationConfigFile['presentation']>;
|
|
64
|
+
|
|
65
|
+
function hasPresentationRoot(parsed: AgentConfigFile) {
|
|
66
|
+
return parsed.presentation !== undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function readPresentationConfigFile(filePath: string): Promise<PresentationConfigLayer> {
|
|
70
|
+
try {
|
|
71
|
+
const rawText = await readFile(filePath, 'utf8');
|
|
72
|
+
const parsed = JSON.parse(rawText) as AgentConfigFile;
|
|
73
|
+
const presentationFile: PresentationConfigFile = hasPresentationRoot(parsed)
|
|
74
|
+
? parsed
|
|
75
|
+
: { presentation: parsed as LegacyPresentationConfigFile };
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
path: filePath,
|
|
79
|
+
exists: true,
|
|
80
|
+
rawConfig: extractPresentationConfigOverride(presentationFile),
|
|
81
|
+
rawBackends: extractBackendConfigOverride(parsed)
|
|
82
|
+
};
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if ((error as NodeJS.ErrnoException | undefined)?.code === 'ENOENT') {
|
|
85
|
+
return { path: filePath, exists: false };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
89
|
+
return {
|
|
90
|
+
path: filePath,
|
|
91
|
+
exists: true,
|
|
92
|
+
error: message
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function serializePresentationConfigOverride(config: PresentationConfigOverride): PresentationConfigFile {
|
|
98
|
+
const presentation: NonNullable<PresentationConfigFile['presentation']> = {};
|
|
99
|
+
|
|
100
|
+
if (config.defaultMode) {
|
|
101
|
+
presentation.defaultMode = config.defaultMode;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (Object.keys(config.tools).length > 0) {
|
|
105
|
+
presentation.tools = config.tools;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { presentation };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function serializeBackendConfigOverride(config: BackendConfigOverride): BackendConfigFile {
|
|
112
|
+
const backends: NonNullable<BackendConfigFile['backends']> = {};
|
|
113
|
+
|
|
114
|
+
if (config.search && Object.keys(config.search).length > 0) {
|
|
115
|
+
backends.search = { ...config.search };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (config.fetch && Object.keys(config.fetch).length > 0) {
|
|
119
|
+
const { apiKey: _apiKey, ...fetch } = config.fetch;
|
|
120
|
+
backends.fetch = { ...fetch };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (config.headless && Object.keys(config.headless).length > 0) {
|
|
124
|
+
backends.headless = { ...config.headless };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { backends };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function readConfigFileForWrite(filePath: string): Promise<AgentConfigFile> {
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(await readFile(filePath, 'utf8')) as AgentConfigFile;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if ((error as NodeJS.ErrnoException | undefined)?.code === 'ENOENT') {
|
|
135
|
+
return {};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function writeConfigFile(filePath: string, config: AgentConfigFile) {
|
|
143
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
144
|
+
await writeFile(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function loadPresentationConfigLayers(
|
|
148
|
+
options: PresentationConfigStoreOptions = {}
|
|
149
|
+
): Promise<LoadedPresentationConfig> {
|
|
150
|
+
const { globalPath, projectPath } = getPresentationConfigPaths(options);
|
|
151
|
+
const global = await readPresentationConfigFile(globalPath);
|
|
152
|
+
const project = await readPresentationConfigFile(projectPath);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
global,
|
|
156
|
+
project,
|
|
157
|
+
effectiveConfig: mergePresentationConfigLayers(
|
|
158
|
+
DEFAULT_PRESENTATION_CONFIG,
|
|
159
|
+
global.rawConfig,
|
|
160
|
+
project.rawConfig
|
|
161
|
+
),
|
|
162
|
+
effectiveBackends: mergeBackendConfigLayers(
|
|
163
|
+
DEFAULT_BACKEND_CONFIG,
|
|
164
|
+
global.rawBackends,
|
|
165
|
+
project.rawBackends
|
|
166
|
+
)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function savePresentationConfigScope(
|
|
171
|
+
options: PresentationConfigStoreOptions,
|
|
172
|
+
scope: PresentationScope,
|
|
173
|
+
config: PresentationConfigOverride
|
|
174
|
+
) {
|
|
175
|
+
const { globalPath, projectPath } = getPresentationConfigPaths(options);
|
|
176
|
+
const filePath = scope === 'global' ? globalPath : projectPath;
|
|
177
|
+
const existing = await readConfigFileForWrite(filePath);
|
|
178
|
+
|
|
179
|
+
await writeConfigFile(filePath, {
|
|
180
|
+
...existing,
|
|
181
|
+
...serializePresentationConfigOverride(config)
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function saveBackendConfigScope(
|
|
186
|
+
options: PresentationConfigStoreOptions,
|
|
187
|
+
scope: PresentationScope,
|
|
188
|
+
config: BackendConfigOverride
|
|
189
|
+
) {
|
|
190
|
+
const { globalPath, projectPath } = getPresentationConfigPaths(options);
|
|
191
|
+
const filePath = scope === 'global' ? globalPath : projectPath;
|
|
192
|
+
const existing = await readConfigFileForWrite(filePath);
|
|
193
|
+
|
|
194
|
+
await writeConfigFile(filePath, {
|
|
195
|
+
...existing,
|
|
196
|
+
...serializeBackendConfigOverride(config)
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function resetPresentationConfigScope(
|
|
201
|
+
options: PresentationConfigStoreOptions,
|
|
202
|
+
scope: PresentationScope
|
|
203
|
+
) {
|
|
204
|
+
const { globalPath, projectPath } = getPresentationConfigPaths(options);
|
|
205
|
+
const filePath = scope === 'global' ? globalPath : projectPath;
|
|
206
|
+
|
|
207
|
+
await rm(filePath, { force: true });
|
|
208
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PRESENTATION_MODES,
|
|
3
|
+
type PresentationConfig,
|
|
4
|
+
type PresentationConfigFile,
|
|
5
|
+
type PresentationConfigOverride,
|
|
6
|
+
type PresentationMode,
|
|
7
|
+
type PresentationToolName
|
|
8
|
+
} from './types.js';
|
|
9
|
+
|
|
10
|
+
const PRESENTATION_MODE_SET = new Set<string>(PRESENTATION_MODES);
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_PRESENTATION_CONFIG: PresentationConfig = {
|
|
13
|
+
defaultMode: 'compact',
|
|
14
|
+
tools: {}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function isPresentationMode(value: unknown): value is PresentationMode {
|
|
18
|
+
return typeof value === 'string' && PRESENTATION_MODE_SET.has(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function extractPresentationConfigOverride(
|
|
22
|
+
file: PresentationConfigFile | null | undefined
|
|
23
|
+
): PresentationConfigOverride {
|
|
24
|
+
const presentation = file?.presentation;
|
|
25
|
+
const tools = Object.fromEntries(
|
|
26
|
+
Object.entries(presentation?.tools ?? {}).flatMap(([toolName, value]) => {
|
|
27
|
+
if (!value || !isPresentationMode(value.mode)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return [[toolName, { mode: value.mode }]];
|
|
32
|
+
})
|
|
33
|
+
) as PresentationConfig['tools'];
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
defaultMode: isPresentationMode(presentation?.defaultMode)
|
|
37
|
+
? presentation.defaultMode
|
|
38
|
+
: undefined,
|
|
39
|
+
tools
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizePresentationConfigFile(
|
|
44
|
+
file: PresentationConfigFile | null | undefined
|
|
45
|
+
): PresentationConfig {
|
|
46
|
+
const override = extractPresentationConfigOverride(file);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
defaultMode: override.defaultMode ?? DEFAULT_PRESENTATION_CONFIG.defaultMode,
|
|
50
|
+
tools: override.tools
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function mergePresentationConfigLayers(
|
|
55
|
+
defaults: PresentationConfig,
|
|
56
|
+
globalConfig?: PresentationConfigOverride,
|
|
57
|
+
projectConfig?: PresentationConfigOverride
|
|
58
|
+
): PresentationConfig {
|
|
59
|
+
return {
|
|
60
|
+
defaultMode:
|
|
61
|
+
projectConfig?.defaultMode ?? globalConfig?.defaultMode ?? defaults.defaultMode,
|
|
62
|
+
tools: {
|
|
63
|
+
...defaults.tools,
|
|
64
|
+
...globalConfig?.tools,
|
|
65
|
+
...projectConfig?.tools
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolvePresentationMode(
|
|
71
|
+
toolName: PresentationToolName,
|
|
72
|
+
config: PresentationConfig = DEFAULT_PRESENTATION_CONFIG
|
|
73
|
+
): PresentationMode {
|
|
74
|
+
return config.tools[toolName]?.mode ?? config.defaultMode;
|
|
75
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { WebExploreResponse } from '../types.js';
|
|
2
|
+
import type { PresentationEnvelope } from './types.js';
|
|
3
|
+
|
|
4
|
+
function internalReaderLabel(method?: 'http' | 'headless' | 'firecrawl') {
|
|
5
|
+
if (method === 'headless') return 'web_fetch_headless';
|
|
6
|
+
if (method === 'firecrawl') return 'firecrawl';
|
|
7
|
+
if (method === 'http') return 'web_fetch';
|
|
8
|
+
return 'web_explore';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function buildExplorePresentation(result: WebExploreResponse): PresentationEnvelope {
|
|
12
|
+
if (result.status === 'error') {
|
|
13
|
+
return {
|
|
14
|
+
mode: 'compact',
|
|
15
|
+
views: {
|
|
16
|
+
compact: `Research failed: ${result.error?.message ?? 'Unknown research failure.'}`
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const internalSummary = result.metadata
|
|
22
|
+
? `Internal research: web_search ×${result.metadata.searchPasses}, web_fetch ×${result.metadata.fetchedPages}, web_fetch_headless ×${result.metadata.headlessAttempts}`
|
|
23
|
+
: undefined;
|
|
24
|
+
const hasEvidence = result.findings.length > 0 || result.sources.length > 0;
|
|
25
|
+
const evidenceLines = hasEvidence
|
|
26
|
+
? result.findings.map((finding, index) => `- [${internalReaderLabel(result.sources[index]?.method)}] ${finding}`)
|
|
27
|
+
: ['No usable evidence found.'];
|
|
28
|
+
const preview = [
|
|
29
|
+
...evidenceLines,
|
|
30
|
+
internalSummary ? `\n${internalSummary}` : undefined
|
|
31
|
+
]
|
|
32
|
+
.filter((line) => line !== undefined)
|
|
33
|
+
.join('\n');
|
|
34
|
+
const verbose = [
|
|
35
|
+
'Findings',
|
|
36
|
+
...evidenceLines,
|
|
37
|
+
'',
|
|
38
|
+
'Sources',
|
|
39
|
+
...result.sources.map((source) => `- [${internalReaderLabel(source.method)}] ${source.title}: ${source.url}`),
|
|
40
|
+
internalSummary ? `\nInternal tools\n${internalSummary}` : undefined,
|
|
41
|
+
result.caveat ? `\nCaveat\n${result.caveat}` : undefined
|
|
42
|
+
]
|
|
43
|
+
.filter((line) => line !== undefined)
|
|
44
|
+
.join('\n');
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
mode: 'compact',
|
|
48
|
+
views: {
|
|
49
|
+
compact: hasEvidence
|
|
50
|
+
? `Reviewed ${result.sources.length} sources · synthesized answer with ${result.findings.length} findings`
|
|
51
|
+
: 'No usable evidence found',
|
|
52
|
+
preview,
|
|
53
|
+
verbose
|
|
54
|
+
},
|
|
55
|
+
metrics: {
|
|
56
|
+
sourceCount: result.sources.length,
|
|
57
|
+
resultCount: result.findings.length
|
|
58
|
+
},
|
|
59
|
+
sources: result.sources
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { WebFetchHeadlessResponse, WebFetchResponse } from '../types.js';
|
|
2
|
+
import type { PresentationEnvelope } from './types.js';
|
|
3
|
+
|
|
4
|
+
type FetchLike = WebFetchResponse | WebFetchHeadlessResponse;
|
|
5
|
+
|
|
6
|
+
function countWords(text: string | undefined): number | undefined {
|
|
7
|
+
return text?.trim() ? text.trim().split(/\s+/).length : undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function firstExcerpt(text: string | undefined, maxChars = 240): string | undefined {
|
|
11
|
+
if (!text) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return text.length <= maxChars ? text : `${text.slice(0, maxChars).trimEnd()}…`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildFetchPresentation(result: FetchLike): PresentationEnvelope {
|
|
19
|
+
const wordCount = countWords(result.content?.text);
|
|
20
|
+
const fallbackPrefix = result.metadata.fallbackFrom
|
|
21
|
+
? `${result.metadata.fallbackFrom} failed; used ${result.metadata.method} fallback. `
|
|
22
|
+
: '';
|
|
23
|
+
const compact =
|
|
24
|
+
result.status === 'ok'
|
|
25
|
+
? `${fallbackPrefix}Fetched page · article extracted${wordCount ? ` · ${wordCount} words` : ''}`
|
|
26
|
+
: result.status === 'needs_headless'
|
|
27
|
+
? `${fallbackPrefix}Needs headless rendering: ${result.error?.message ?? 'Headless rendering recommended.'}`
|
|
28
|
+
: `${fallbackPrefix}Fetch failed: ${result.error?.message ?? 'Unknown fetch failure.'}`;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
mode: 'compact',
|
|
32
|
+
views: {
|
|
33
|
+
compact,
|
|
34
|
+
preview: result.content?.title
|
|
35
|
+
? `${result.content.title}\n${firstExcerpt(result.content.text) ?? ''}`.trim()
|
|
36
|
+
: firstExcerpt(result.content?.text),
|
|
37
|
+
verbose:
|
|
38
|
+
result.status === 'ok'
|
|
39
|
+
? [
|
|
40
|
+
`URL: ${result.url}`,
|
|
41
|
+
result.content?.title ? `Title: ${result.content.title}` : undefined,
|
|
42
|
+
firstExcerpt(result.content?.text, 500)
|
|
43
|
+
]
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.join('\n')
|
|
46
|
+
: undefined
|
|
47
|
+
},
|
|
48
|
+
metrics: {
|
|
49
|
+
wordCount,
|
|
50
|
+
cacheHit: result.metadata.cacheHit
|
|
51
|
+
},
|
|
52
|
+
sources: [{ title: result.content?.title ?? result.url, url: result.url }]
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { WebSearchResponse } from '../types.js';
|
|
2
|
+
import type { PresentationEnvelope } from './types.js';
|
|
3
|
+
|
|
4
|
+
function formatCompact(result: WebSearchResponse): string {
|
|
5
|
+
const fallbackPrefix = result.metadata.fallbackFrom
|
|
6
|
+
? `${result.metadata.fallbackFrom} failed; used ${result.metadata.backend} fallback. `
|
|
7
|
+
: '';
|
|
8
|
+
|
|
9
|
+
if (result.status === 'error') {
|
|
10
|
+
return `${fallbackPrefix}Search failed: ${result.error?.message ?? 'Unknown search failure.'}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const suffix = result.results.length === 1 ? 'result' : 'results';
|
|
14
|
+
return `${fallbackPrefix}Found ${result.results.length} ${suffix}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildSearchPresentation(result: WebSearchResponse): PresentationEnvelope {
|
|
18
|
+
const preview = result.results
|
|
19
|
+
.slice(0, 3)
|
|
20
|
+
.map((item, index) => `${index + 1}. ${item.title}`)
|
|
21
|
+
.join('\n');
|
|
22
|
+
|
|
23
|
+
const verbose = result.results
|
|
24
|
+
.slice(0, 5)
|
|
25
|
+
.map((item, index) => `${index + 1}. ${item.title}\n ${item.url}\n ${item.snippet}`)
|
|
26
|
+
.join('\n');
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
mode: 'compact',
|
|
30
|
+
views: {
|
|
31
|
+
compact: formatCompact(result),
|
|
32
|
+
preview: preview || undefined,
|
|
33
|
+
verbose: verbose || undefined
|
|
34
|
+
},
|
|
35
|
+
metrics: {
|
|
36
|
+
resultCount: result.results.length,
|
|
37
|
+
cacheHit: result.metadata.cacheHit
|
|
38
|
+
},
|
|
39
|
+
sources: result.results.map((item) => ({ title: item.title, url: item.url }))
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PresentationEnvelope, PresentationMode } from './types.js';
|
|
2
|
+
|
|
3
|
+
export function selectPresentationView(
|
|
4
|
+
envelope: PresentationEnvelope | undefined,
|
|
5
|
+
requestedMode: PresentationMode
|
|
6
|
+
): string | undefined {
|
|
7
|
+
if (!envelope) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (requestedMode === 'preview' && envelope.views.preview) {
|
|
12
|
+
return envelope.views.preview;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (requestedMode === 'verbose' && envelope.views.verbose) {
|
|
16
|
+
return envelope.views.verbose;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return envelope.views.compact;
|
|
20
|
+
}
|