@fleetagent/pi-coding-agent 0.0.2 → 0.0.4

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.
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
16
  import { basename, dirname } from "node:path";
17
- import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, streamSimple, } from "@fleetagent/pi-ai";
17
+ import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, streamSimple, validateToolArguments, } from "@fleetagent/pi-ai";
18
18
  import { theme } from "../modes/interactive/theme/theme.js";
19
19
  import { stripFrontmatter } from "../utils/frontmatter.js";
20
20
  import { resolvePath } from "../utils/paths.js";
@@ -27,6 +27,7 @@ import { exportSessionToHtml } from "./export-html/index.js";
27
27
  import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
28
28
  import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
29
29
  import { emitSessionShutdownEvent } from "./extensions/runner.js";
30
+ import { STRUCTURED_RESPONSE_INTERNAL_CUSTOM_TYPE } from "./messages.js";
30
31
  import { expandPromptTemplate } from "./prompt-templates.js";
31
32
  import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
32
33
  import { createSyntheticSourceInfo } from "./source-info.js";
@@ -54,6 +55,41 @@ export function parseSkillBlock(text) {
54
55
  // ============================================================================
55
56
  /** Standard thinking levels */
56
57
  const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
58
+ const DEFAULT_STRUCTURED_RESPONSE_TOOL_NAME = "structured_output";
59
+ const DEFAULT_STRUCTURED_RESPONSE_CORRECTIONS = 2;
60
+ function isRecord(value) {
61
+ return typeof value === "object" && value !== null && !Array.isArray(value);
62
+ }
63
+ function getAssistantText(message) {
64
+ return message.content
65
+ .filter((block) => block.type === "text")
66
+ .map((block) => block.text)
67
+ .join("\n");
68
+ }
69
+ function extractJsonCandidates(text) {
70
+ const candidates = [];
71
+ const trimmed = text.trim();
72
+ if (trimmed) {
73
+ try {
74
+ candidates.push(JSON.parse(trimmed));
75
+ }
76
+ catch {
77
+ // Try fenced JSON blocks below.
78
+ }
79
+ }
80
+ const fencePattern = /```(?:json)?\s*([\s\S]*?)```/gi;
81
+ let match = fencePattern.exec(text);
82
+ while (match) {
83
+ try {
84
+ candidates.push(JSON.parse(match[1].trim()));
85
+ }
86
+ catch {
87
+ // Ignore invalid fenced blocks.
88
+ }
89
+ match = fencePattern.exec(text);
90
+ }
91
+ return candidates;
92
+ }
57
93
  // ============================================================================
58
94
  // AgentSession Class
59
95
  // ============================================================================
@@ -674,6 +710,186 @@ export class AgentSession {
674
710
  }
675
711
  return await this._checkCompaction(msg);
676
712
  }
713
+ async getStructuredResponse(options) {
714
+ if (this.isStreaming) {
715
+ throw new Error("Agent is already processing. Wait for completion before requesting structured output.");
716
+ }
717
+ if (!this.model) {
718
+ throw new Error(formatNoModelSelectedMessage());
719
+ }
720
+ if (!this._modelRegistry.hasConfiguredAuth(this.model)) {
721
+ throw new Error(formatNoApiKeyFoundMessage(this.model.provider));
722
+ }
723
+ const schemaName = options.name ?? DEFAULT_STRUCTURED_RESPONSE_TOOL_NAME;
724
+ const tool = {
725
+ name: schemaName,
726
+ description: options.description ??
727
+ "Return the requested structured response. Call this tool exactly once with arguments matching the schema.",
728
+ parameters: options.schema,
729
+ };
730
+ const lastAssistant = this._findLastAssistantMessage();
731
+ if (!lastAssistant) {
732
+ throw new Error("No assistant response is available to structure.");
733
+ }
734
+ const direct = this._tryParseStructuredAssistantText(tool, lastAssistant);
735
+ if (direct.ok) {
736
+ this._appendStructuredInternalEntry("result", schemaName, 0, "Validated structured response from assistant JSON.", {
737
+ stage: "result",
738
+ schemaName,
739
+ attempt: 0,
740
+ source: "json",
741
+ });
742
+ return { output: direct.output, attempts: 0, source: "json", message: lastAssistant };
743
+ }
744
+ const maxCorrections = options.maxCorrections ?? DEFAULT_STRUCTURED_RESPONSE_CORRECTIONS;
745
+ const maxAttempts = maxCorrections + 1;
746
+ const { apiKey, headers } = await this._getRequiredRequestAuth(this.model);
747
+ const messages = this._buildStructuredResponseMessages(options.scope ?? "latest", lastAssistant, schemaName);
748
+ let lastError = direct.error;
749
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
750
+ const requestText = attempt === 1
751
+ ? `Extract the structured response by calling ${schemaName} exactly once.`
752
+ : `Correct the structured response by calling ${schemaName} exactly once.\n\nValidation error:\n${lastError}`;
753
+ const userMessage = {
754
+ role: "user",
755
+ content: [{ type: "text", text: requestText }],
756
+ timestamp: Date.now(),
757
+ };
758
+ messages.push(userMessage);
759
+ this._appendStructuredInternalEntry("request", schemaName, attempt, requestText, {
760
+ stage: "request",
761
+ schemaName,
762
+ attempt,
763
+ validationError: attempt === 1 ? undefined : lastError,
764
+ });
765
+ const responseStream = await this.agent.streamFn(this.model, {
766
+ systemPrompt: "You are a structured data extraction assistant. Do not answer in prose. Use the provided tool to return the structured data.",
767
+ messages,
768
+ tools: [tool],
769
+ }, {
770
+ apiKey,
771
+ headers,
772
+ sessionId: this.agent.sessionId,
773
+ transport: this.agent.transport,
774
+ thinkingBudgets: this.agent.thinkingBudgets,
775
+ maxRetryDelayMs: this.agent.maxRetryDelayMs,
776
+ });
777
+ const assistantMessage = await responseStream.result();
778
+ messages.push(assistantMessage);
779
+ this._appendStructuredInternalEntry("assistant", schemaName, attempt, this._formatStructuredAssistantLog(assistantMessage), {
780
+ stage: "assistant",
781
+ schemaName,
782
+ attempt,
783
+ });
784
+ const toolCall = assistantMessage.content.find((block) => block.type === "toolCall" && block.name === schemaName);
785
+ if (toolCall) {
786
+ const validation = this._validateStructuredArguments(tool, toolCall.arguments);
787
+ if (validation.ok) {
788
+ this._appendStructuredInternalEntry("result", schemaName, attempt, "Validated structured tool response.", {
789
+ stage: "result",
790
+ schemaName,
791
+ attempt,
792
+ source: "tool",
793
+ });
794
+ return { output: validation.output, attempts: attempt, source: "tool", message: assistantMessage };
795
+ }
796
+ lastError = validation.error;
797
+ const toolResult = {
798
+ role: "toolResult",
799
+ toolCallId: toolCall.id,
800
+ toolName: toolCall.name,
801
+ content: [{ type: "text", text: validation.error }],
802
+ isError: true,
803
+ timestamp: Date.now(),
804
+ };
805
+ messages.push(toolResult);
806
+ this._appendStructuredInternalEntry("tool_result", schemaName, attempt, validation.error, {
807
+ stage: "tool_result",
808
+ schemaName,
809
+ attempt,
810
+ validationError: validation.error,
811
+ });
812
+ continue;
813
+ }
814
+ const textValidation = this._tryParseStructuredAssistantText(tool, assistantMessage);
815
+ if (textValidation.ok) {
816
+ this._appendStructuredInternalEntry("result", schemaName, attempt, "Validated structured JSON response.", {
817
+ stage: "result",
818
+ schemaName,
819
+ attempt,
820
+ source: "json",
821
+ });
822
+ return { output: textValidation.output, attempts: attempt, source: "json", message: assistantMessage };
823
+ }
824
+ lastError = textValidation.error;
825
+ }
826
+ throw new Error(`Structured response validation failed after ${maxAttempts} attempt(s):\n${lastError}`);
827
+ }
828
+ _buildStructuredResponseMessages(scope, lastAssistant, schemaName) {
829
+ if (scope === "conversation") {
830
+ return this.agent.state.messages.filter((message) => message.role === "user" || message.role === "assistant" || message.role === "toolResult");
831
+ }
832
+ return [
833
+ {
834
+ role: "user",
835
+ content: [
836
+ {
837
+ type: "text",
838
+ text: `Extract structured data from the latest assistant response below. Call ${schemaName} exactly once.\n\n` +
839
+ `<assistant_response>\n${getAssistantText(lastAssistant)}\n</assistant_response>`,
840
+ },
841
+ ],
842
+ timestamp: Date.now(),
843
+ },
844
+ ];
845
+ }
846
+ _tryParseStructuredAssistantText(tool, message) {
847
+ const text = getAssistantText(message);
848
+ for (const candidate of extractJsonCandidates(text)) {
849
+ if (!isRecord(candidate)) {
850
+ continue;
851
+ }
852
+ const validation = this._validateStructuredArguments(tool, candidate);
853
+ if (validation.ok) {
854
+ return validation;
855
+ }
856
+ }
857
+ return { ok: false, error: "Assistant response did not contain valid JSON matching the requested schema." };
858
+ }
859
+ _validateStructuredArguments(tool, arguments_) {
860
+ try {
861
+ const output = validateToolArguments(tool, {
862
+ type: "toolCall",
863
+ id: "structured-response-validation",
864
+ name: tool.name,
865
+ arguments: arguments_,
866
+ });
867
+ return { ok: true, output };
868
+ }
869
+ catch (error) {
870
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
871
+ }
872
+ }
873
+ _appendStructuredInternalEntry(stage, schemaName, attempt, content, details) {
874
+ this.session.appendCustomMessageEntry(STRUCTURED_RESPONSE_INTERNAL_CUSTOM_TYPE, content, false, {
875
+ ...details,
876
+ stage,
877
+ schemaName,
878
+ attempt,
879
+ });
880
+ }
881
+ _formatStructuredAssistantLog(message) {
882
+ const parts = message.content.map((block) => {
883
+ if (block.type === "text") {
884
+ return block.text;
885
+ }
886
+ if (block.type === "thinking") {
887
+ return "[thinking omitted]";
888
+ }
889
+ return `tool:${block.name} ${JSON.stringify(block.arguments)}`;
890
+ });
891
+ return parts.join("\n");
892
+ }
677
893
  /**
678
894
  * Send a prompt to the agent.
679
895
  * - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming