Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ Proma 的 Agent 模式提供两套可切换的内核:

- **Claude Agent Runtime(默认)**:基于 `@anthropic-ai/claude-agent-sdk`,使用 Anthropic Messages API 或兼容端点。
- **Pi Agent Runtime**:基于 `@earendil-works/pi-coding-agent`、`pi-agent-core` 和 `pi-ai`,将 Proma 的已启用渠道动态注册为 Pi provider;支持 OpenAI Chat Completions / Responses、Google Generative AI、Anthropic Messages 及其兼容端点。
- **Pi Goal 模式**:在 Pi Agent 会话中使用 `/goal <任务>` 持久保存目标并跨 turn 自动继续;可用 `/goal stop` 停止,完成后由 Agent 调用 `goal_complete`,并受 50 轮安全上限保护。

| 渠道类型 | Chat | Claude Agent | Pi Agent |
| --- | --- | --- | --- |
Expand Down
2 changes: 1 addition & 1 deletion apps/electron/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@proma/electron",
"version": "0.15.10",
"version": "0.15.11",
"description": "Proma next gen ai software with general agents - Electron App",
"main": "dist/main.cjs",
"author": {
Expand Down
26 changes: 26 additions & 0 deletions apps/electron/src/main/lib/adapters/pi-agent-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, expect, test } from 'bun:test'
import { buildPiExtensionFactories } from './pi-extension-factories'

describe('Pi extension factories', () => {
test('always loads the Proma Goal extension', () => {
const factories = buildPiExtensionFactories({
provider: 'openai',
modelReasoning: false,
modelId: 'gpt-4o',
})

expect(factories).toHaveLength(1)
})

test('preserves Codex request settings alongside the Goal extension', () => {
const factories = buildPiExtensionFactories({
provider: 'openai-codex',
modelReasoning: true,
modelId: 'gpt-5.6-sol',
codexFastMode: true,
openAIThinkingLevel: 'high',
})

expect(factories).toHaveLength(2)
})
})
18 changes: 10 additions & 8 deletions apps/electron/src/main/lib/adapters/pi-agent-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ import {
type AgentRuntimeGuard,
} from '../agent-runtime-guards'
import { createPromaAgentsFilesOverride } from './pi-resource-loader-overrides'
import { createCodexRequestSettingsExtension, withCodexFastModeServiceTier } from './pi-codex-request-settings'
import { withCodexFastModeServiceTier } from './pi-codex-request-settings'
import { waitForGoalCommandTurn } from './pi-command-lifecycle'
import { buildPiExtensionFactories } from './pi-extension-factories'
import { mergeRuntimeEnv, type AgentRuntimeEnv } from '../agent-runtime-env'
import {
convertPiMessage,
Expand Down Expand Up @@ -1348,13 +1350,12 @@ export class PiAgentAdapter implements AgentProviderAdapter {
additionalSkillPaths: input.additionalSkillPaths ?? [],
skillsOverride: createPromaSkillsOverride(input.additionalSkillPaths),
agentsFilesOverride: createPromaAgentsFilesOverride(),
...((input.provider === 'openai-codex' || input.provider === 'openai-responses')
&& model.reasoning
&& isOpenAIReasoningSupportedModel(input.model) && {
extensionFactories: [createCodexRequestSettingsExtension({
fastMode: input.codexFastMode,
thinkingLevel: input.openAIThinkingLevel,
})],
extensionFactories: buildPiExtensionFactories({
provider: input.provider,
modelReasoning: model.reasoning,
modelId: input.model,
codexFastMode: input.codexFastMode,
openAIThinkingLevel: input.openAIThinkingLevel,
}),
systemPromptOverride: () => input.systemPrompt,
})
Expand Down Expand Up @@ -1667,6 +1668,7 @@ export class PiAgentAdapter implements AgentProviderAdapter {
}
currentInterrupt?.resolveAccepted()
await session.prompt(prompt, { source: 'rpc' })
await waitForGoalCommandTurn(session, prompt)
persistPiEntryBindings()
if (compactContextRequested) {
await compactCurrentSessionAfterTurn(session, (message) => queue.push(message))
Expand Down
46 changes: 46 additions & 0 deletions apps/electron/src/main/lib/adapters/pi-command-lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, expect, test } from 'bun:test'
import {
isGoalStartCommand,
waitForGoalCommandTurn,
type PiSessionLifecycle,
} from './pi-command-lifecycle'

describe('Pi Goal command lifecycle', () => {
test('recognizes goal starts but reserves exact stop command', () => {
expect(isGoalStartCommand('/goal ship the feature')).toBe(true)
expect(isGoalStartCommand('/goal stop procrastinating')).toBe(true)
expect(isGoalStartCommand('/goal stop')).toBe(false)
expect(isGoalStartCommand('/goal')).toBe(false)
})

test('waits for the nested goal turn after the command handler returns', async () => {
let waited = false
const session: PiSessionLifecycle = {
isStreaming: true,
pendingMessageCount: 0,
waitForIdle: async () => {
waited = true
},
}

await waitForGoalCommandTurn(session, '/goal ship the feature')

expect(waited).toBe(true)
})

test('does not wait for stop or ordinary prompts', async () => {
let waited = false
const session: PiSessionLifecycle = {
isStreaming: true,
pendingMessageCount: 1,
waitForIdle: async () => {
waited = true
},
}

await waitForGoalCommandTurn(session, '/goal stop')
await waitForGoalCommandTurn(session, 'continue the task')

expect(waited).toBe(false)
})
})
22 changes: 22 additions & 0 deletions apps/electron/src/main/lib/adapters/pi-command-lifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { AgentSession } from '@earendil-works/pi-coding-agent'

export type PiSessionLifecycle = Pick<AgentSession, 'isStreaming' | 'pendingMessageCount' | 'waitForIdle'>

export function isGoalStartCommand(prompt: string): boolean {
const normalized = prompt.trim()
return normalized.startsWith('/goal ') && normalized !== '/goal stop'
}

export async function waitForGoalCommandTurn(
session: PiSessionLifecycle,
prompt: string,
): Promise<void> {
if (!isGoalStartCommand(prompt)) return

// ExtensionAPI.sendUserMessage() intentionally does not expose its promise.
// 等一个事件循环,让 Pi 先把内嵌的 Goal turn 标记为 active,再等待它收束。
await new Promise<void>((resolve) => setImmediate(resolve))
if (session.isStreaming || session.pendingMessageCount > 0) {
await session.waitForIdle()
}
}
29 changes: 29 additions & 0 deletions apps/electron/src/main/lib/adapters/pi-extension-factories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type {
InlineExtension,
} from '@earendil-works/pi-coding-agent'
import type { AgentThinkingLevel, ProviderType } from '@proma/shared'
import { isOpenAIReasoningSupportedModel } from '@proma/shared'
import { createCodexRequestSettingsExtension } from './pi-codex-request-settings'
import { createPromaGoalExtension } from './pi-goal-extension'

export interface PiExtensionFactoryOptions {
provider: ProviderType
modelReasoning: boolean
modelId?: string
codexFastMode?: boolean
openAIThinkingLevel?: AgentThinkingLevel
}

export function buildPiExtensionFactories(options: PiExtensionFactoryOptions): InlineExtension[] {
const extensionFactories: InlineExtension[] = [createPromaGoalExtension()]
const usesCodexResponses = options.provider === 'openai-codex' || options.provider === 'openai-responses'

if (usesCodexResponses && options.modelReasoning && isOpenAIReasoningSupportedModel(options.modelId)) {
extensionFactories.push(createCodexRequestSettingsExtension({
fastMode: options.codexFastMode,
thinkingLevel: options.openAIThinkingLevel,
}))
}

return extensionFactories
}
Loading