diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 97cc6477..fcdee700 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,10 +3,17 @@ "isRoot": true, "tools": { "fantomas": { - "version": "6.3.0-alpha-004", + "version": "7.0.5", "commands": [ "fantomas" ] + }, + "fsdocs-tool": { + "version": "21.0.0", + "commands": [ + "fsdocs" + ], + "rollForward": false } } } \ No newline at end of file diff --git a/.fantomasignore b/.fantomasignore new file mode 100644 index 00000000..12b05f3c --- /dev/null +++ b/.fantomasignore @@ -0,0 +1,5 @@ +# Literate documentation scripts are not subject to Fantomas formatting +docs/ + +# Generated output from fsdocs +output/ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..c1965c21 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json new file mode 100644 index 00000000..41cfccbb --- /dev/null +++ b/.github/aw/actions-lock.json @@ -0,0 +1,51 @@ +{ + "entries": { + "actions/github-script@v9.0.0": { + "repo": "actions/github-script", + "version": "v9.0.0", + "sha": "d746ffe35508b1917358783b479e04febd2b8f71" + }, + "github/gh-aw-actions/setup@v0.68.3": { + "repo": "github/gh-aw-actions/setup", + "version": "v0.68.3", + "sha": "ba90f2186d7ad780ec640f364005fa24e797b360" + }, + "github/gh-aw/actions/setup@v0.68.7": { + "repo": "github/gh-aw/actions/setup", + "version": "v0.68.7", + "sha": "f916d5de5199f770e46151d455ab1f0288981cc9" + } + }, + "containers": { + "ghcr.io/github/gh-aw-firewall/agent:0.25.20": { + "image": "ghcr.io/github/gh-aw-firewall/agent:0.25.20", + "digest": "sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682", + "pinned_image": "ghcr.io/github/gh-aw-firewall/agent:0.25.20@sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682" + }, + "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20": { + "image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20", + "digest": "sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519", + "pinned_image": "ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20@sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519" + }, + "ghcr.io/github/gh-aw-firewall/squid:0.25.20": { + "image": "ghcr.io/github/gh-aw-firewall/squid:0.25.20", + "digest": "sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236", + "pinned_image": "ghcr.io/github/gh-aw-firewall/squid:0.25.20@sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236" + }, + "ghcr.io/github/gh-aw-mcpg:v0.2.19": { + "image": "ghcr.io/github/gh-aw-mcpg:v0.2.19", + "digest": "sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd", + "pinned_image": "ghcr.io/github/gh-aw-mcpg:v0.2.19@sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd" + }, + "ghcr.io/github/github-mcp-server:v0.32.0": { + "image": "ghcr.io/github/github-mcp-server:v0.32.0", + "digest": "sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28", + "pinned_image": "ghcr.io/github/github-mcp-server:v0.32.0@sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28" + }, + "node:lts-alpine": { + "image": "node:lts-alpine", + "digest": "sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f", + "pinned_image": "node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f" + } + } +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a4b4c3d1..9ca08f36 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,6 +6,9 @@ updates: # ignore all patch and pre-release updates - dependency-name: "*" update-types: ["version-update:semver-patch"] + # ignore updates to gh-aw-actions, which only appears in auto-generated *.lock.yml + # files managed by 'gh aw compile' and should not be touched by dependabot + - dependency-name: "github/gh-aw-actions" schedule: interval: daily open-pull-requests-limit: 10 diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d4bd5061..ae28c8d6 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -9,12 +9,19 @@ jobs: steps: - name: checkout-code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: setup-dotnet uses: actions/setup-dotnet@v4 + + - name: Cache NuGet packages + uses: actions/cache@v5 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('**/*.fsproj', '**/*.csproj', 'global.json') }} + restore-keys: nuget-${{ runner.os }}- - name: tool restore run: dotnet tool restore @@ -28,7 +35,7 @@ jobs: steps: # checkout the code - name: checkout-code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -36,6 +43,14 @@ jobs: - name: setup-dotnet uses: actions/setup-dotnet@v4 + # cache NuGet packages to avoid re-downloading on every run + - name: Cache NuGet packages + uses: actions/cache@v5 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('**/*.fsproj', '**/*.csproj', 'global.json') }} + restore-keys: nuget-${{ runner.os }}- + # build it, test it, pack it - name: Run dotnet build (release) # see issue #105 diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 00000000..e547a024 --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,46 @@ +name: Build and Deploy Docs + +on: + push: + branches: + - main + +permissions: + contents: write # needed for peaceiris/actions-gh-pages + +jobs: + docs: + name: Build and deploy docs + runs-on: ubuntu-latest + steps: + - name: checkout-code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: setup-dotnet + uses: actions/setup-dotnet@v4 + + - name: Cache NuGet packages + uses: actions/cache@v5 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('**/*.fsproj', 'global.json') }} + restore-keys: nuget-${{ runner.os }}- + + - name: Install tools + run: dotnet tool restore + + - name: Build library (Release) + run: dotnet build src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj -c Release + + - name: Build docs + run: dotnet fsdocs build --properties Configuration=Release --eval + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./output + publish_branch: gh-pages + force_orphan: true diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index f9899cc5..1a384ed1 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -12,12 +12,19 @@ jobs: steps: # checkout the code - name: checkout-code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 # setup dotnet based on global.json - name: setup-dotnet uses: actions/setup-dotnet@v4 + # cache NuGet packages to avoid re-downloading on every run + - name: Cache NuGet packages + uses: actions/cache@v5 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('**/*.fsproj', '**/*.csproj', 'global.json') }} + restore-keys: nuget-${{ runner.os }}- # build it, test it, pack it - name: Run dotnet build (release) # see issue #105 @@ -31,12 +38,19 @@ jobs: steps: # checkout the code - name: checkout-code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 # setup dotnet based on global.json - name: setup-dotnet uses: actions/setup-dotnet@v4 + # cache NuGet packages to avoid re-downloading on every run + - name: Cache NuGet packages + uses: actions/cache@v5 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('**/*.fsproj', '**/*.csproj', 'global.json') }} + restore-keys: nuget-${{ runner.os }}- # build it, test it, pack it - name: Run dotnet test - release # see issue #105 @@ -44,7 +58,7 @@ jobs: shell: cmd run: ./build.cmd ci -release - name: Publish test results - release - uses: dorny/test-reporter@v1 + uses: dorny/test-reporter@v2 if: always() with: name: Report release tests diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 8821447b..1f46124b 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -5,6 +5,10 @@ on: branches: - main +permissions: + #contents: write # for peaceiris/actions-gh-pages + id-token: write # for NuGet trusted publishing + jobs: publish: name: Publish nuget (if new version) @@ -12,25 +16,30 @@ jobs: steps: # checkout the code - name: checkout-code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 # setup dotnet based on global.json - name: setup-dotnet uses: actions/setup-dotnet@v4 + # cache NuGet packages to avoid re-downloading on every run + - name: Cache NuGet packages + uses: actions/cache@v5 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('**/*.fsproj', '**/*.csproj', 'global.json') }} + restore-keys: nuget-${{ runner.os }}- # build it, test it, pack it, publish it - name: Run dotnet build (release, for nuget) # see issue #105 and #243 # very important, since we use cmd scripts, the default is psh, and a bug prevents errorlevel to bubble shell: cmd run: ./build.cmd - - name: Nuget publish - # skip-duplicate ensures that the 409 error received when the package was already published, - # will just issue a warning and won't have the GH action fail. - # NUGET_PUBLISH_TOKEN_TASKSEQ is valid until approx. 11 Dec 2024 and will need to be updated by then: - # - log in to Nuget.org using 'abelbraaksma' admin account and then refresh the token in Nuget - # - copy the token - # - go to https://github.com/fsprojects/FSharp.Control.TaskSeq/settings/secrets/actions - # - select button "Add repository secret" or update the existing one under "Repository secrets" - # - rerun the job - run: dotnet nuget push packages\FSharp.Control.TaskSeq.*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_PUBLISH_TOKEN_TASKSEQ }} --skip-duplicate + - name: Obtain NuGet key + # this hash is v1.1.0 + uses: NuGet/login@d22cc5f58ff5b88bf9bd452535b4335137e24544 + id: login + with: + user: dsyme + - name: Publish NuGets (if this version not published before) + run: dotnet nuget push packages\FSharp.Control.TaskSeq.*.nupkg -s https://www.nuget.org/api/v2/package -k ${{ steps.login.outputs.NUGET_API_KEY }} --skip-duplicate diff --git a/.github/workflows/repo-assist.lock.yml b/.github/workflows/repo-assist.lock.yml new file mode 100644 index 00000000..ae8b235a --- /dev/null +++ b/.github/workflows/repo-assist.lock.yml @@ -0,0 +1,1722 @@ +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"e063003610075034aa5bb03fcb3e403675bcd287771df466417d97799e24d9d8","compiler_version":"v0.68.3","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"ba90f2186d7ad780ec640f364005fa24e797b360","version":"v0.68.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20","digest":"sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.25.20@sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20","digest":"sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20@sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20","digest":"sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.25.20@sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.19","digest":"sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.2.19@sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd"},{"image":"ghcr.io/github/github-mcp-server:v0.32.0","digest":"sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28","pinned_image":"ghcr.io/github/github-mcp-server:v0.32.0@sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.68.3). DO NOT EDIT. +# +# To update this file, edit githubnext/agentics/workflows/repo-assist.md@96b9d4c39aa22359c0b38265927eadb31dcf4e2a and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# A friendly repository assistant that runs 2 times a day to support contributors and maintainers. +# Can also be triggered on-demand via '/repo-assist ' to perform specific tasks. +# - Labels and triages open issues +# - Comments helpfully on open issues to unblock contributors and onboard newcomers +# - Identifies issues that can be fixed and creates draft pull requests with fixes +# - Improves performance, testing, and code quality via PRs +# - Makes engineering investments: dependency updates, CI improvements, tooling +# - Updates its own PRs when CI fails or merge conflicts arise +# - Nudges stale PRs waiting for author response +# - Takes the repository forward with proactive improvements +# - Maintains a persistent memory of work done and what remains +# Always polite, constructive, and mindful of the project's goals. +# +# Source: githubnext/agentics/workflows/repo-assist.md@96b9d4c39aa22359c0b38265927eadb31dcf4e2a +# +# Secrets used: +# - COPILOT_GITHUB_TOKEN +# - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_GITHUB_MCP_SERVER_TOKEN +# - GH_AW_GITHUB_TOKEN +# - GITHUB_TOKEN +# +# Custom actions used: +# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 +# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 +# - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 +# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 +# - github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 +# +# Container images used: +# - ghcr.io/github/gh-aw-firewall/agent:0.25.20@sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20@sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.20@sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236 +# - ghcr.io/github/gh-aw-mcpg:v0.2.19@sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd +# - ghcr.io/github/github-mcp-server:v0.32.0@sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28 +# - node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + +name: "Repo Assist" +"on": + discussion: + types: + - created + - edited + discussion_comment: + types: + - created + - edited + issue_comment: + types: + - created + - edited + issues: + types: + - opened + - edited + - reopened + pull_request: + types: + - opened + - edited + - reopened + pull_request_review_comment: + types: + - created + - edited + schedule: + - cron: "34 */48 * * *" + workflow_dispatch: + inputs: + aw_context: + default: "" + description: Agent caller context (used internally by Agentic Workflows). + required: false + type: string + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}" + +run-name: "Repo Assist" + +jobs: + activation: + needs: pre_activation + if: "needs.pre_activation.outputs.activated == 'true' && ((github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment') && (github.event_name == 'issues' && (startsWith(github.event.issue.body, '/repo-assist ') || startsWith(github.event.issue.body, '/repo-assist\n') || github.event.issue.body == '/repo-assist') || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/repo-assist ') || startsWith(github.event.comment.body, '/repo-assist\n') || github.event.comment.body == '/repo-assist') && github.event.issue.pull_request == null || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/repo-assist ') || startsWith(github.event.comment.body, '/repo-assist\n') || github.event.comment.body == '/repo-assist') && github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' && (startsWith(github.event.comment.body, '/repo-assist ') || startsWith(github.event.comment.body, '/repo-assist\n') || github.event.comment.body == '/repo-assist') || github.event_name == 'pull_request' && (startsWith(github.event.pull_request.body, '/repo-assist ') || startsWith(github.event.pull_request.body, '/repo-assist\n') || github.event.pull_request.body == '/repo-assist') || github.event_name == 'discussion' && (startsWith(github.event.discussion.body, '/repo-assist ') || startsWith(github.event.discussion.body, '/repo-assist\n') || github.event.discussion.body == '/repo-assist') || github.event_name == 'discussion_comment' && (startsWith(github.event.comment.body, '/repo-assist ') || startsWith(github.event.comment.body, '/repo-assist\n') || github.event.comment.body == '/repo-assist')) || (!(github.event_name == 'issues')) && (!(github.event_name == 'issue_comment')) && (!(github.event_name == 'pull_request')) && (!(github.event_name == 'pull_request_review_comment')) && (!(github.event_name == 'discussion')) && (!(github.event_name == 'discussion_comment')))" + runs-on: ubuntu-slim + permissions: + actions: read + contents: read + discussions: write + issues: write + pull-requests: write + outputs: + body: ${{ steps.sanitized.outputs.body }} + comment_id: ${{ steps.add-comment.outputs.comment-id }} + comment_repo: ${{ steps.add-comment.outputs.comment-repo }} + comment_url: ${{ steps.add-comment.outputs.comment-url }} + lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + slash_command: ${{ needs.pre_activation.outputs.matched_command }} + stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + text: ${{ steps.sanitized.outputs.text }} + title: ${{ steps.sanitized.outputs.title }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} + - name: Generate agentic run info + id: generate_aw_info + env: + GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.21" + GH_AW_INFO_AGENT_VERSION: "1.0.21" + GH_AW_INFO_CLI_VERSION: "v0.68.3" + GH_AW_INFO_WORKFLOW_NAME: "Repo Assist" + GH_AW_INFO_EXPERIMENTAL: "false" + GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" + GH_AW_INFO_STAGED: "false" + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dotnet","node","python","rust","java"]' + GH_AW_INFO_FIREWALL_ENABLED: "true" + GH_AW_INFO_AWF_VERSION: "v0.25.20" + GH_AW_INFO_AWMG_VERSION: "" + GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_COMPILED_STRICT: "true" + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + await main(core, context); + - name: Add eyes reaction for immediate feedback + id: react + if: github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.id == github.repository_id + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_REACTION: "eyes" + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/add_reaction.cjs'); + await main(); + - name: Validate COPILOT_GITHUB_TOKEN secret + id: validate-secret + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: | + .github + .agents + sparse-checkout-cone-mode: true + fetch-depth: 1 + - name: Check workflow lock file + id: check-lock-file + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_WORKFLOW_FILE: "repo-assist.lock.yml" + GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Check compile-agentic version + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_COMPILED_VERSION: "v0.68.3" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + await main(); + - name: Compute current body text + id: sanitized + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); + await main(); + - name: Add comment with workflow run link + id: add-comment + if: github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.id == github.repository_id + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_WORKFLOW_NAME: "Repo Assist" + GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e Generated by 🌈 {workflow_name}, see [workflow run]({run_url}). [Learn more](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md).\",\"runStarted\":\"{workflow_name} is processing {event_type}, see [workflow run]({run_url})...\",\"runSuccess\":\"✓ {workflow_name} completed successfully, see [workflow run]({run_url}).\",\"runFailure\":\"✗ {workflow_name} encountered {status}, see [workflow run]({run_url}).\"}" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/add_workflow_run_comment.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_SERVER_URL: ${{ github.server_url }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }} + GH_AW_STEPS_SANITIZED_OUTPUTS_TEXT: ${{ steps.sanitized.outputs.text }} + GH_AW_WIKI_NOTE: ${{ '' }} + # poutine:ignore untrusted_checkout_exec + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" + { + cat << 'GH_AW_PROMPT_d1cb9c255dc8ced9_EOF' + + GH_AW_PROMPT_d1cb9c255dc8ced9_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/repo_memory_prompt.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" + cat << 'GH_AW_PROMPT_d1cb9c255dc8ced9_EOF' + + Tools: add_comment(max:10), create_issue(max:4), update_issue, create_pull_request(max:4), add_labels(max:30), remove_labels(max:5), push_to_pull_request_branch(max:4), missing_tool, missing_data, noop + GH_AW_PROMPT_d1cb9c255dc8ced9_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" + cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" + cat << 'GH_AW_PROMPT_d1cb9c255dc8ced9_EOF' + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + - **checkouts**: The following repositories have been checked out and are available in the workspace: + - `$GITHUB_WORKSPACE` → `__GH_AW_GITHUB_REPOSITORY__` (cwd) [full history, all branches available as remote-tracking refs] [additional refs fetched: *] + - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it without proper authentication. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches). + + + GH_AW_PROMPT_d1cb9c255dc8ced9_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" + if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then + cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" + fi + if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then + cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_push_to_pr_branch_guidance.md" + fi + cat << 'GH_AW_PROMPT_d1cb9c255dc8ced9_EOF' + + {{#runtime-import .github/workflows/repo-assist.md}} + GH_AW_PROMPT_d1cb9c255dc8ced9_EOF + } > "$GH_AW_PROMPT" + - name: Interpolate variables and render templates + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_SERVER_URL: ${{ github.server_url }} + GH_AW_STEPS_SANITIZED_OUTPUTS_TEXT: ${{ steps.sanitized.outputs.text }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_SERVER_URL: ${{ github.server_url }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_IS_PR_COMMENT: ${{ github.event.issue.pull_request && 'true' || '' }} + GH_AW_MEMORY_BRANCH_NAME: 'memory/repo-assist' + GH_AW_MEMORY_CONSTRAINTS: "\n\n**Constraints:**\n- **Max File Size**: 10240 bytes (0.01 MB) per file\n- **Max File Count**: 100 files per commit\n- **Max Patch Size**: 10240 bytes (10 KB) total per push (max: 100 KB)\n" + GH_AW_MEMORY_DESCRIPTION: '' + GH_AW_MEMORY_DIR: '/tmp/gh-aw/repo-memory/default/' + GH_AW_MEMORY_TARGET_REPO: ' of the current repository' + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: ${{ needs.pre_activation.outputs.matched_command }} + GH_AW_STEPS_SANITIZED_OUTPUTS_TEXT: ${{ steps.sanitized.outputs.text }} + GH_AW_WIKI_NOTE: '' + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + + const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_SERVER_URL: process.env.GH_AW_GITHUB_SERVER_URL, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_IS_PR_COMMENT: process.env.GH_AW_IS_PR_COMMENT, + GH_AW_MEMORY_BRANCH_NAME: process.env.GH_AW_MEMORY_BRANCH_NAME, + GH_AW_MEMORY_CONSTRAINTS: process.env.GH_AW_MEMORY_CONSTRAINTS, + GH_AW_MEMORY_DESCRIPTION: process.env.GH_AW_MEMORY_DESCRIPTION, + GH_AW_MEMORY_DIR: process.env.GH_AW_MEMORY_DIR, + GH_AW_MEMORY_TARGET_REPO: process.env.GH_AW_MEMORY_TARGET_REPO, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND, + GH_AW_STEPS_SANITIZED_OUTPUTS_TEXT: process.env.GH_AW_STEPS_SANITIZED_OUTPUTS_TEXT, + GH_AW_WIKI_NOTE: process.env.GH_AW_WIKI_NOTE + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + # poutine:ignore untrusted_checkout_exec + run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Upload activation artifact + if: success() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: activation + path: | + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/github_rate_limits.jsonl + if-no-files-found: ignore + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: read-all + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_WORKFLOW_ID_SANITIZED: repoassist + outputs: + agentic_engine_timeout: ${{ steps.detect-copilot-errors.outputs.agentic_engine_timeout || 'false' }} + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + inference_access_error: ${{ steps.detect-copilot-errors.outputs.inference_access_error || 'false' }} + mcp_policy_error: ${{ steps.detect-copilot-errors.outputs.mcp_policy_error || 'false' }} + model: ${{ needs.activation.outputs.model }} + model_not_supported_error: ${{ steps.detect-copilot-errors.outputs.model_not_supported_error || 'false' }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Set runtime paths + id: set-runtime-paths + run: | + { + echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" + echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" + echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" + } >> "$GITHUB_OUTPUT" + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + fetch-depth: 0 + - name: Fetch additional refs + env: + GH_AW_FETCH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + header=$(printf "x-access-token:%s" "${GH_AW_FETCH_TOKEN}" | base64 -w 0) + git -c "http.extraheader=Authorization: Basic ${header}" fetch origin '+refs/heads/*:refs/remotes/origin/*' + - name: Create gh-aw temp directory + run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" + - name: Configure gh CLI for GitHub Enterprise + run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" + env: + GH_TOKEN: ${{ github.token }} + - name: Start DIFC proxy for pre-agent gh calls + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_SERVER_URL: ${{ github.server_url }} + DIFC_PROXY_POLICY: '{"allow-only":{"min-integrity":"none","repos":"all"}}' + DIFC_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.2.19' + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/start_difc_proxy.sh" + - name: Set GH_REPO for proxied steps + run: | + echo "GH_REPO=${GITHUB_REPOSITORY}" >> "$GITHUB_ENV" + - env: + GH_TOKEN: ${{ github.token }} + name: Fetch repo data for task weighting + run: "mkdir -p /tmp/gh-aw\n\n# Fetch open issues with labels (up to 500)\ngh issue list --state open --limit 500 --json number,labels > /tmp/gh-aw/issues.json\n\n# Fetch open PRs with titles (up to 200)\ngh pr list --state open --limit 200 --json number,title > /tmp/gh-aw/prs.json\n\n# Compute task weights and select two tasks for this run\npython3 - << 'EOF'\nimport json, random, os\n\nwith open('/tmp/gh-aw/issues.json') as f:\n issues = json.load(f)\nwith open('/tmp/gh-aw/prs.json') as f:\n prs = json.load(f)\n\nopen_issues = len(issues)\nunlabelled = sum(1 for i in issues if not i.get('labels'))\nrepo_assist_prs = sum(1 for p in prs if p['title'].startswith('[Repo Assist]'))\nother_prs = sum(1 for p in prs if not p['title'].startswith('[Repo Assist]'))\n\ntask_names = {\n 1: 'Issue Labelling',\n 2: 'Issue Investigation and Comment',\n 3: 'Issue Investigation and Fix',\n 4: 'Engineering Investments',\n 5: 'Coding Improvements',\n 6: 'Maintain Repo Assist PRs',\n 7: 'Stale PR Nudges',\n 8: 'Performance Improvements',\n 9: 'Testing Improvements',\n 10: 'Take the Repository Forward',\n}\n\nweights = {\n 1: 1 + 3 * unlabelled,\n 2: 3 + 1 * open_issues,\n 3: 3 + 0.7 * open_issues,\n 4: 5 + 0.2 * open_issues,\n 5: 5 + 0.1 * open_issues,\n 6: float(repo_assist_prs),\n 7: 0.1 * other_prs,\n 8: 3 + 0.05 * open_issues,\n 9: 3 + 0.05 * open_issues,\n 10: 3 + 0.05 * open_issues,\n}\n\n# Seed with run ID for reproducibility within a run\nrun_id = int(os.environ.get('GITHUB_RUN_ID', '0'))\nrng = random.Random(run_id)\n\ntask_ids = list(weights.keys())\ntask_weights = [weights[t] for t in task_ids]\n\n# Weighted sample without replacement (pick 2 distinct tasks)\nchosen, seen = [], set()\nfor t in rng.choices(task_ids, weights=task_weights, k=30):\n if t not in seen:\n seen.add(t)\n chosen.append(t)\n if len(chosen) == 2:\n break\n\nprint('=== Repo Assist Task Selection ===')\nprint(f'Open issues : {open_issues}')\nprint(f'Unlabelled issues : {unlabelled}')\nprint(f'Repo Assist PRs : {repo_assist_prs}')\nprint(f'Other open PRs : {other_prs}')\nprint()\nprint('Task weights:')\nfor t, w in weights.items():\n tag = ' <-- SELECTED' if t in chosen else ''\n print(f' Task {t:2d} ({task_names[t]}): weight {w:6.1f}{tag}')\nprint()\nprint(f'Selected tasks for this run: Task {chosen[0]} ({task_names[chosen[0]]}) and Task {chosen[1]} ({task_names[chosen[1]]})')\n\nresult = {\n 'open_issues': open_issues, 'unlabelled_issues': unlabelled,\n 'repo_assist_prs': repo_assist_prs, 'other_prs': other_prs,\n 'task_names': task_names,\n 'weights': {str(k): round(v, 2) for k, v in weights.items()},\n 'selected_tasks': chosen,\n}\nwith open('/tmp/gh-aw/task_selection.json', 'w') as f:\n json.dump(result, f, indent=2)\nEOF\n" + + # Repo memory git-based storage configuration from frontmatter processed below + - name: Clone repo-memory branch (default) + env: + GH_TOKEN: ${{ github.token }} + GITHUB_SERVER_URL: ${{ github.server_url }} + BRANCH_NAME: memory/repo-assist + TARGET_REPO: ${{ github.repository }} + MEMORY_DIR: /tmp/gh-aw/repo-memory/default + CREATE_ORPHAN: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clone_repo_memory_branch.sh" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request || github.event.issue.pull_request + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.20 + - name: Parse integrity filter lists + id: parse-guard-vars + env: + GH_AW_BLOCKED_USERS_VAR: ${{ vars.GH_AW_GITHUB_BLOCKED_USERS || '' }} + GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} + GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" + - name: Stop DIFC proxy + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/stop_difc_proxy.sh" + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.20@sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20@sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519 ghcr.io/github/gh-aw-firewall/squid:0.25.20@sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236 ghcr.io/github/gh-aw-mcpg:v0.2.19@sha256:44d4d8de7e6c37aaea484eba489940c52df6a0b54078ddcbc9327592d5b3c3dd ghcr.io/github/github-mcp-server:v0.32.0@sha256:2763823c63bcca718ce53850a1d7fcf2f501ec84028394f1b63ce7e9f4f9be28 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f + - name: Write Safe Outputs Config + run: | + mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_fcd7e1a3fa6998f1_EOF' + {"add_comment":{"hide_older_comments":true,"max":10,"target":"*"},"add_labels":{"allowed":["bug","enhancement","help wanted","good first issue","spam","off topic","documentation","question","duplicate","wontfix","needs triage","needs investigation","breaking change","performance","security","refactor"],"max":30,"target":"*"},"create_issue":{"labels":["automation","repo-assist"],"max":4,"title_prefix":"[Repo Assist] "},"create_pull_request":{"draft":true,"labels":["automation","repo-assist"],"max":4,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"title_prefix":"[Repo Assist] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"push_repo_memory":{"memories":[{"dir":"/tmp/gh-aw/repo-memory/default","id":"default","max_file_count":100,"max_file_size":10240,"max_patch_size":10240}]},"push_to_pull_request_branch":{"if_no_changes":"warn","max":4,"max_patch_size":1024,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS"],"protected_files_policy":"fallback-to-issue","protected_path_prefixes":[".github/",".agents/"],"target":"*","title_prefix":"[Repo Assist] "},"remove_labels":{"allowed":["bug","enhancement","help wanted","good first issue","spam","off topic","documentation","question","duplicate","wontfix","needs triage","needs investigation","breaking change","performance","security","refactor"],"max":5,"target":"*"},"report_incomplete":{},"update_issue":{"allow_body":true,"max":1,"target":"*","title_prefix":"[Repo Assist] "}} + GH_AW_SAFE_OUTPUTS_CONFIG_fcd7e1a3fa6998f1_EOF + - name: Write Safe Outputs Tools + env: + GH_AW_TOOLS_META_JSON: | + { + "description_suffixes": { + "add_comment": " CONSTRAINTS: Maximum 10 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", + "add_labels": " CONSTRAINTS: Maximum 30 label(s) can be added. Only these labels are allowed: [\"bug\" \"enhancement\" \"help wanted\" \"good first issue\" \"spam\" \"off topic\" \"documentation\" \"question\" \"duplicate\" \"wontfix\" \"needs triage\" \"needs investigation\" \"breaking change\" \"performance\" \"security\" \"refactor\"]. Target: *.", + "create_issue": " CONSTRAINTS: Maximum 4 issue(s) can be created. Title will be prefixed with \"[Repo Assist] \". Labels [\"automation\" \"repo-assist\"] will be automatically added.", + "create_pull_request": " CONSTRAINTS: Maximum 4 pull request(s) can be created. Title will be prefixed with \"[Repo Assist] \". Labels [\"automation\" \"repo-assist\"] will be automatically added. PRs will be created as drafts.", + "push_to_pull_request_branch": " CONSTRAINTS: Maximum 4 push(es) can be made. The target pull request title must start with \"[Repo Assist] \".", + "remove_labels": " CONSTRAINTS: Maximum 5 label(s) can be removed. Only these labels can be removed: [bug enhancement help wanted good first issue spam off topic documentation question duplicate wontfix needs triage needs investigation breaking change performance security refactor]. Target: *.", + "update_issue": " CONSTRAINTS: Maximum 1 issue(s) can be updated. Target: *. The target issue title must start with \"[Repo Assist] \"." + }, + "repo_params": {}, + "dynamic_tools": [] + } + GH_AW_VALIDATION_JSON: | + { + "add_comment": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "item_number": { + "issueOrPRNumber": true + }, + "reply_to_id": { + "type": "string", + "maxLength": 256 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "add_labels": { + "defaultMax": 5, + "fields": { + "item_number": { + "issueNumberOrTemporaryId": true + }, + "labels": { + "required": true, + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "create_issue": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "parent": { + "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "temporary_id": { + "type": "string" + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "create_pull_request": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "draft": { + "type": "boolean" + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_data": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "context": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "data_type": { + "type": "string", + "sanitize": true, + "maxLength": 128 + }, + "reason": { + "type": "string", + "sanitize": true, + "maxLength": 256 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + }, + "push_to_pull_request_branch": { + "defaultMax": 1, + "fields": { + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "pull_request_number": { + "issueOrPRNumber": true + } + } + }, + "remove_labels": { + "defaultMax": 5, + "fields": { + "item_number": { + "issueNumberOrTemporaryId": true + }, + "labels": { + "required": true, + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, + "report_incomplete": { + "defaultMax": 5, + "fields": { + "details": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 1024 + } + } + }, + "update_issue": { + "defaultMax": 1, + "fields": { + "assignees": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 39 + }, + "body": { + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "issue_number": { + "issueOrPRNumber": true + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "milestone": { + "optionalPositiveInteger": true + }, + "operation": { + "type": "string", + "enum": [ + "replace", + "append", + "prepend", + "replace-island" + ] + }, + "repo": { + "type": "string", + "maxLength": 256 + }, + "status": { + "type": "string", + "enum": [ + "open", + "closed" + ] + }, + "title": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + }, + "customValidation": "requiresOneOf:status,title,body" + } + } + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + await main(); + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh" + + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export DEBUG="*" + + export GH_AW_ENGINE="copilot" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.19' + + mkdir -p /home/runner/.copilot + cat << GH_AW_MCP_CONFIG_47f4351d5b673cda_EOF | bash "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh" + { + "mcpServers": { + "github": { + "type": "stdio", + "container": "ghcr.io/github/github-mcp-server:v0.32.0", + "env": { + "GITHUB_HOST": "\${GITHUB_SERVER_URL}", + "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "all" + }, + "guard-policies": { + "allow-only": { + "approval-labels": ${{ steps.parse-guard-vars.outputs.approval_labels }}, + "blocked-users": ${{ steps.parse-guard-vars.outputs.blocked_users }}, + "min-integrity": "none", + "repos": "all", + "trusted-users": ${{ steps.parse-guard-vars.outputs.trusted_users }} + } + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}" + }, + "guard-policies": { + "write-sink": { + "accept": [ + "*" + ] + } + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_47f4351d5b673cda_EOF + - name: Download activation artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw + - name: Clean git credentials + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" + - name: Execute GitHub Copilot CLI + id: agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 60 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + (umask 177 && touch /tmp/gh-aw/agent-stdio.log) + # shellcheck disable=SC1003 + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains '*.gradle-enterprise.cloud,*.pythonhosted.org,*.vsblob.vsassets.io,adoptium.net,anaconda.org,api.adoptium.net,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.foojay.io,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.nuget.org,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,binstar.org,bootstrap.pypa.io,builds.dotnet.microsoft.com,bun.sh,cdn.azul.com,cdn.jsdelivr.net,central.sonatype.com,ci.dot.net,conda.anaconda.org,conda.binstar.org,crates.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,deb.nodesource.com,deno.land,develocity.apache.org,dist.nuget.org,dl.google.com,dlcdn.apache.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,esm.sh,files.pythonhosted.org,ge.spockframework.org,get.pnpm.io,github.com,googleapis.deno.dev,googlechromelabs.github.io,gradle.org,host.docker.internal,index.crates.io,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven-central.storage-download.googleapis.com,maven.apache.org,maven.google.com,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pip.pypa.io,pkgs.dev.azure.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.anaconda.com,repo.continuum.io,repo.gradle.org,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,repository.apache.org,s.symcb.com,s.symcd.com,scans-in.gradle.com,security.ubuntu.com,services.gradle.org,sh.rustup.rs,skimdb.npmjs.com,static.crates.io,static.rust-lang.org,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.java.com,www.microsoft.com,www.npmjs.com,www.npmjs.org,yarnpkg.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.20 --skip-pull --enable-api-proxy \ + -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || '' }} + GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json + GH_AW_PHASE: agent + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_VERSION: v0.68.3 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Detect Copilot errors + id: detect-copilot-errors + if: always() + continue-on-error: true + run: node "${RUNNER_TEMP}/gh-aw/actions/detect_copilot_errors.cjs" + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Copy Copilot session state files to logs + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Append agent step summary + if: always() + run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" + - name: Copy Safe Outputs + if: always() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: | + mkdir -p /tmp/gh-aw + cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true + - name: Ingest agent output + id: collect_output + if: always() + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,*.pythonhosted.org,*.vsblob.vsassets.io,adoptium.net,anaconda.org,api.adoptium.net,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.foojay.io,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.nuget.org,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,binstar.org,bootstrap.pypa.io,builds.dotnet.microsoft.com,bun.sh,cdn.azul.com,cdn.jsdelivr.net,central.sonatype.com,ci.dot.net,conda.anaconda.org,conda.binstar.org,crates.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,deb.nodesource.com,deno.land,develocity.apache.org,dist.nuget.org,dl.google.com,dlcdn.apache.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,esm.sh,files.pythonhosted.org,ge.spockframework.org,get.pnpm.io,github.com,googleapis.deno.dev,googlechromelabs.github.io,gradle.org,host.docker.internal,index.crates.io,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven-central.storage-download.googleapis.com,maven.apache.org,maven.google.com,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pip.pypa.io,pkgs.dev.azure.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.anaconda.com,repo.continuum.io,repo.gradle.org,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,repository.apache.org,s.symcb.com,s.symcd.com,scans-in.gradle.com,security.ubuntu.com,services.gradle.org,sh.rustup.rs,skimdb.npmjs.com,static.crates.io,static.rust-lang.org,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.java.com,www.microsoft.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_COMMAND: repo-assist + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + id: parse-mcp-gateway + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Parse token usage for step summary + if: always() + continue-on-error: true + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + await main(); + - name: Write agent output placeholder if missing + if: always() + run: | + if [ ! -f /tmp/gh-aw/agent_output.json ]; then + echo '{"items":[]}' > /tmp/gh-aw/agent_output.json + fi + # Upload repo memory as artifacts for push job + - name: Upload repo-memory artifact (default) + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: repo-memory-default + path: /tmp/gh-aw/repo-memory/default + retention-days: 1 + if-no-files-found: ignore + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/sandbox/agent/logs/ + /tmp/gh-aw/redacted-urls.log + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/proxy-logs/ + !/tmp/gh-aw/proxy-logs/proxy-tls/ + /tmp/gh-aw/agent_usage.json + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/safeoutputs.jsonl + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/aw-*.patch + /tmp/gh-aw/aw-*.bundle + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/sandbox/firewall/audit/ + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - push_repo_memory + - safe_outputs + if: > + always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || + needs.activation.outputs.stale_lock_file_failed == 'true') + runs-on: ubuntu-slim + permissions: + contents: write + discussions: write + issues: write + pull-requests: write + concurrency: + group: "gh-aw-conclusion-repo-assist" + cancel-in-progress: false + outputs: + incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Process no-op messages + id: noop + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: "1" + GH_AW_WORKFLOW_NAME: "Repo Assist" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-assist.md@96b9d4c39aa22359c0b38265927eadb31dcf4e2a" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/96b9d4c39aa22359c0b38265927eadb31dcf4e2a/workflows/repo-assist.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Log detection run + id: detection_runs + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Repo Assist" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-assist.md@96b9d4c39aa22359c0b38265927eadb31dcf4e2a" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/96b9d4c39aa22359c0b38265927eadb31dcf4e2a/workflows/repo-assist.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + await main(); + - name: Record missing tool + id: missing_tool + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Repo Assist" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-assist.md@96b9d4c39aa22359c0b38265927eadb31dcf4e2a" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/96b9d4c39aa22359c0b38265927eadb31dcf4e2a/workflows/repo-assist.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Record incomplete + id: report_incomplete + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" + GH_AW_WORKFLOW_NAME: "Repo Assist" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-assist.md@96b9d4c39aa22359c0b38265927eadb31dcf4e2a" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/96b9d4c39aa22359c0b38265927eadb31dcf4e2a/workflows/repo-assist.md" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + await main(); + - name: Handle agent failure + id: handle_agent_failure + if: always() + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Repo Assist" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-assist.md@96b9d4c39aa22359c0b38265927eadb31dcf4e2a" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/96b9d4c39aa22359c0b38265927eadb31dcf4e2a/workflows/repo-assist.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "repo-assist" + GH_AW_ENGINE_ID: "copilot" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} + GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} + GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} + GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} + GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} + GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} + GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} + GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} + GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e Generated by 🌈 {workflow_name}, see [workflow run]({run_url}). [Learn more](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md).\",\"runStarted\":\"{workflow_name} is processing {event_type}, see [workflow run]({run_url})...\",\"runSuccess\":\"✓ {workflow_name} completed successfully, see [workflow run]({run_url}).\",\"runFailure\":\"✗ {workflow_name} encountered {status}, see [workflow run]({run_url}).\"}" + GH_AW_PUSH_REPO_MEMORY_RESULT: ${{ needs.push_repo_memory.result }} + GH_AW_REPO_MEMORY_VALIDATION_FAILED_default: ${{ needs.push_repo_memory.outputs.validation_failed_default }} + GH_AW_REPO_MEMORY_VALIDATION_ERROR_default: ${{ needs.push_repo_memory.outputs.validation_error_default }} + GH_AW_REPO_MEMORY_PATCH_SIZE_EXCEEDED_default: ${{ needs.push_repo_memory.outputs.patch_size_exceeded_default }} + GH_AW_GROUP_REPORTS: "false" + GH_AW_FAILURE_REPORT_AS_ISSUE: "true" + GH_AW_TIMEOUT_MINUTES: "60" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Repo Assist" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e Generated by 🌈 {workflow_name}, see [workflow run]({run_url}). [Learn more](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md).\",\"runStarted\":\"{workflow_name} is processing {event_type}, see [workflow run]({run_url})...\",\"runSuccess\":\"✓ {workflow_name} completed successfully, see [workflow run]({run_url}).\",\"runFailure\":\"✗ {workflow_name} encountered {status}, see [workflow run]({run_url}).\"}" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/notify_comment_error.cjs'); + await main(); + + detection: + needs: + - activation + - agent + if: > + always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true') + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} + detection_reason: ${{ steps.detection_conclusion.outputs.reason }} + detection_success: ${{ steps.detection_conclusion.outputs.success }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Checkout repository for patch context + if: needs.agent.outputs.has_patch == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + # --- Threat Detection --- + - name: Clean stale firewall files from agent artifact + run: | + rm -rf /tmp/gh-aw/sandbox/firewall/logs + rm -rf /tmp/gh-aw/sandbox/firewall/audit + - name: Download container images + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.20@sha256:9161f2415a3306a344aca34dd671ee69f122317e0a512e66dc64c94b9c508682 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.20@sha256:6971639e381e82e45134bcd333181f456df3a52cd6f818a3e3d6de068ff91519 ghcr.io/github/gh-aw-firewall/squid:0.25.20@sha256:5411d903f73ee597e6a084971c2adef3eb0bd405910df3ed7bf5e3d6bd58a236 + - name: Check if detection needed + id: detection_guard + if: always() + env: + OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + run: | + if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then + echo "run_detection=true" >> "$GITHUB_OUTPUT" + echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" + else + echo "run_detection=false" >> "$GITHUB_OUTPUT" + echo "Detection skipped: no agent outputs or patches to analyze" + fi + - name: Clear MCP configuration for detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + rm -f /tmp/gh-aw/mcp-config/mcp-servers.json + rm -f /home/runner/.copilot/mcp-config.json + rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" + - name: Prepare threat detection files + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection/aw-prompts + cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true + cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true + for f in /tmp/gh-aw/aw-*.patch; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + for f in /tmp/gh-aw/aw-*.bundle; do + [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true + done + echo "Prepared threat detection files:" + ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true + - name: Setup threat detection + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + WORKFLOW_NAME: "Repo Assist" + WORKFLOW_DESCRIPTION: "A friendly repository assistant that runs 2 times a day to support contributors and maintainers.\nCan also be triggered on-demand via '/repo-assist ' to perform specific tasks.\n- Labels and triages open issues\n- Comments helpfully on open issues to unblock contributors and onboard newcomers\n- Identifies issues that can be fixed and creates draft pull requests with fixes\n- Improves performance, testing, and code quality via PRs\n- Makes engineering investments: dependency updates, CI improvements, tooling\n- Updates its own PRs when CI fails or merge conflicts arise\n- Nudges stale PRs waiting for author response\n- Takes the repository forward with proactive improvements\n- Maintains a persistent memory of work done and what remains\nAlways polite, constructive, and mindful of the project's goals." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Install GitHub Copilot CLI + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.21 + env: + GH_HOST: github.com + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.20 + - name: Execute GitHub Copilot CLI + if: always() && steps.detection_guard.outputs.run_detection == 'true' + id: detection_agentic_execution + # Copilot CLI tool arguments (sorted): + timeout-minutes: 20 + run: | + set -o pipefail + touch /tmp/gh-aw/agent-step-summary.md + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + # shellcheck disable=SC1003 + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,telemetry.enterprise.githubcopilot.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.20 --skip-pull --enable-api-proxy \ + -- /bin/bash -c 'node ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + COPILOT_AGENT_RUNNER_TYPE: STANDALONE + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} + GH_AW_PHASE: detection + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_VERSION: v0.68.3 + GITHUB_API_URL: ${{ github.api_url }} + GITHUB_AW: true + GITHUB_HEAD_REF: ${{ github.head_ref }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md + GITHUB_WORKSPACE: ${{ github.workspace }} + GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_AUTHOR_NAME: github-actions[bot] + GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com + GIT_COMMITTER_NAME: github-actions[bot] + XDG_CONFIG_HOME: /home/runner + - name: Upload threat detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + - name: Parse and conclude threat detection + id: detection_conclusion + if: always() + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + + pre_activation: + if: "(github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment') && (github.event_name == 'issues' && (startsWith(github.event.issue.body, '/repo-assist ') || startsWith(github.event.issue.body, '/repo-assist\n') || github.event.issue.body == '/repo-assist') || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/repo-assist ') || startsWith(github.event.comment.body, '/repo-assist\n') || github.event.comment.body == '/repo-assist') && github.event.issue.pull_request == null || github.event_name == 'issue_comment' && (startsWith(github.event.comment.body, '/repo-assist ') || startsWith(github.event.comment.body, '/repo-assist\n') || github.event.comment.body == '/repo-assist') && github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' && (startsWith(github.event.comment.body, '/repo-assist ') || startsWith(github.event.comment.body, '/repo-assist\n') || github.event.comment.body == '/repo-assist') || github.event_name == 'pull_request' && (startsWith(github.event.pull_request.body, '/repo-assist ') || startsWith(github.event.pull_request.body, '/repo-assist\n') || github.event.pull_request.body == '/repo-assist') || github.event_name == 'discussion' && (startsWith(github.event.discussion.body, '/repo-assist ') || startsWith(github.event.discussion.body, '/repo-assist\n') || github.event.discussion.body == '/repo-assist') || github.event_name == 'discussion_comment' && (startsWith(github.event.comment.body, '/repo-assist ') || startsWith(github.event.comment.body, '/repo-assist\n') || github.event.comment.body == '/repo-assist')) || (!(github.event_name == 'issues')) && (!(github.event_name == 'issue_comment')) && (!(github.event_name == 'pull_request')) && (!(github.event_name == 'pull_request_review_comment')) && (!(github.event_name == 'discussion')) && (!(github.event_name == 'discussion_comment'))" + runs-on: ubuntu-slim + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' && steps.check_command_position.outputs.command_position_ok == 'true' }} + matched_command: ${{ steps.check_command_position.outputs.matched_command }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + - name: Check team membership for command workflow + id: check_membership + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_REQUIRED_ROLES: "admin,maintainer,write" + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); + await main(); + - name: Check command position + id: check_command_position + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_COMMANDS: "[\"repo-assist\"]" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_command_position.cjs'); + await main(); + + push_repo_memory: + needs: + - activation + - agent + - detection + if: > + always() && (!cancelled()) && (needs.detection.result == 'success' || needs.detection.result == 'skipped') && + needs.agent.result != 'skipped' + runs-on: ubuntu-slim + permissions: + contents: write + concurrency: + group: "push-repo-memory-${{ github.repository }}|memory/repo-assist" + cancel-in-progress: false + outputs: + patch_size_exceeded_default: ${{ steps.push_repo_memory_default.outputs.patch_size_exceeded }} + validation_error_default: ${{ steps.push_repo_memory_default.outputs.validation_error }} + validation_failed_default: ${{ steps.push_repo_memory_default.outputs.validation_failed }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: . + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GITHUB_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Download repo-memory artifact (default) + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + continue-on-error: true + with: + name: repo-memory-default + path: /tmp/gh-aw/repo-memory/default + - name: Push repo-memory changes (default) + id: push_repo_memory_default + if: always() + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} + GITHUB_SERVER_URL: ${{ github.server_url }} + ARTIFACT_DIR: /tmp/gh-aw/repo-memory/default + MEMORY_ID: default + TARGET_REPO: ${{ github.repository }} + BRANCH_NAME: memory/repo-assist + MAX_FILE_SIZE: 10240 + MAX_FILE_COUNT: 100 + MAX_PATCH_SIZE: 10240 + ALLOWED_EXTENSIONS: '[]' + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/push_repo_memory.cjs'); + await main(); + + safe_outputs: + needs: + - activation + - agent + - detection + if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' + runs-on: ubuntu-slim + permissions: + contents: write + discussions: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/repo-assist" + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} + GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} + GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} + GH_AW_ENGINE_ID: "copilot" + GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e Generated by 🌈 {workflow_name}, see [workflow run]({run_url}). [Learn more](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md).\",\"runStarted\":\"{workflow_name} is processing {event_type}, see [workflow run]({run_url})...\",\"runSuccess\":\"✓ {workflow_name} completed successfully, see [workflow run]({run_url}).\",\"runFailure\":\"✗ {workflow_name} encountered {status}, see [workflow run]({run_url}).\"}" + GH_AW_WORKFLOW_ID: "repo-assist" + GH_AW_WORKFLOW_NAME: "Repo Assist" + GH_AW_WORKFLOW_SOURCE: "githubnext/agentics/workflows/repo-assist.md@96b9d4c39aa22359c0b38265927eadb31dcf4e2a" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/githubnext/agentics/blob/96b9d4c39aa22359c0b38265927eadb31dcf4e2a/workflows/repo-assist.md" + outputs: + code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }} + code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }} + comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} + comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} + created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} + created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} + created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + push_commit_sha: ${{ steps.process_safe_outputs.outputs.push_commit_sha }} + push_commit_url: ${{ steps.process_safe_outputs.outputs.push_commit_url }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@ba90f2186d7ad780ec640f364005fa24e797b360 # v0.68.3 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + trace-id: ${{ needs.activation.outputs.setup-trace-id }} + - name: Download agent output artifact + id: download-agent-output + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Setup agent output environment variable + id: setup-agent-output-env + if: steps.download-agent-output.outcome == 'success' + run: | + mkdir -p /tmp/gh-aw/ + find "/tmp/gh-aw/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: agent + path: /tmp/gh-aw/ + - name: Checkout repository + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') || (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }} + token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + persist-credentials: false + fetch-depth: 1 + - name: Configure Git credentials + if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') || (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + git config --global am.keepcr true + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Configure GH_HOST for enterprise compatibility + id: ghes-host-config + shell: bash + run: | + # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct + # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. + GH_HOST="${GITHUB_SERVER_URL#https://}" + GH_HOST="${GH_HOST#http://}" + echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 + env: + GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} + GH_AW_ALLOWED_DOMAINS: "*.gradle-enterprise.cloud,*.pythonhosted.org,*.vsblob.vsassets.io,adoptium.net,anaconda.org,api.adoptium.net,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.foojay.io,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.nuget.org,api.snapcraft.io,archive.apache.org,archive.ubuntu.com,azure.archive.ubuntu.com,azuresearch-usnc.nuget.org,azuresearch-ussc.nuget.org,binstar.org,bootstrap.pypa.io,builds.dotnet.microsoft.com,bun.sh,cdn.azul.com,cdn.jsdelivr.net,central.sonatype.com,ci.dot.net,conda.anaconda.org,conda.binstar.org,crates.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,dc.services.visualstudio.com,deb.nodesource.com,deno.land,develocity.apache.org,dist.nuget.org,dl.google.com,dlcdn.apache.org,dot.net,dotnet.microsoft.com,dotnetcli.blob.core.windows.net,download.eclipse.org,download.java.net,download.oracle.com,downloads.gradle-dn.com,esm.sh,files.pythonhosted.org,ge.spockframework.org,get.pnpm.io,github.com,googleapis.deno.dev,googlechromelabs.github.io,gradle.org,host.docker.internal,index.crates.io,jcenter.bintray.com,jdk.java.net,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,maven-central.storage-download.googleapis.com,maven.apache.org,maven.google.com,maven.oracle.com,maven.pkg.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,nuget.org,nuget.pkg.github.com,nugetregistryv2prod.blob.core.windows.net,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,oneocsp.microsoft.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pip.pypa.io,pkgs.dev.azure.com,plugins-artifacts.gradle.org,plugins.gradle.org,ppa.launchpad.net,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.anaconda.com,repo.continuum.io,repo.gradle.org,repo.grails.org,repo.maven.apache.org,repo.spring.io,repo.yarnpkg.com,repo1.maven.org,repository.apache.org,s.symcb.com,s.symcd.com,scans-in.gradle.com,security.ubuntu.com,services.gradle.org,sh.rustup.rs,skimdb.npmjs.com,static.crates.io,static.rust-lang.org,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.java.com,www.microsoft.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":10,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"bug\",\"enhancement\",\"help wanted\",\"good first issue\",\"spam\",\"off topic\",\"documentation\",\"question\",\"duplicate\",\"wontfix\",\"needs triage\",\"needs investigation\",\"breaking change\",\"performance\",\"security\",\"refactor\"],\"max\":30,\"target\":\"*\"},\"create_issue\":{\"labels\":[\"automation\",\"repo-assist\"],\"max\":4,\"title_prefix\":\"[Repo Assist] \"},\"create_pull_request\":{\"draft\":true,\"labels\":[\"automation\",\"repo-assist\"],\"max\":4,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"title_prefix\":\"[Repo Assist] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max\":4,\"max_patch_size\":1024,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"AGENTS.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"protected_path_prefixes\":[\".github/\",\".agents/\"],\"target\":\"*\",\"title_prefix\":\"[Repo Assist] \"},\"remove_labels\":{\"allowed\":[\"bug\",\"enhancement\",\"help wanted\",\"good first issue\",\"spam\",\"off topic\",\"documentation\",\"question\",\"duplicate\",\"wontfix\",\"needs triage\",\"needs investigation\",\"breaking change\",\"performance\",\"security\",\"refactor\"],\"max\":5,\"target\":\"*\"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"*\",\"title_prefix\":\"[Repo Assist] \"}}" + GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + - name: Upload Safe Outputs Items + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: safe-outputs-items + path: | + /tmp/gh-aw/safe-output-items.jsonl + /tmp/gh-aw/temporary-id-map.json + if-no-files-found: ignore + diff --git a/.github/workflows/repo-assist.md b/.github/workflows/repo-assist.md new file mode 100644 index 00000000..4baef883 --- /dev/null +++ b/.github/workflows/repo-assist.md @@ -0,0 +1,392 @@ +--- +description: | + A friendly repository assistant that runs 2 times a day to support contributors and maintainers. + Can also be triggered on-demand via '/repo-assist ' to perform specific tasks. + - Labels and triages open issues + - Comments helpfully on open issues to unblock contributors and onboard newcomers + - Identifies issues that can be fixed and creates draft pull requests with fixes + - Improves performance, testing, and code quality via PRs + - Makes engineering investments: dependency updates, CI improvements, tooling + - Updates its own PRs when CI fails or merge conflicts arise + - Nudges stale PRs waiting for author response + - Takes the repository forward with proactive improvements + - Maintains a persistent memory of work done and what remains + Always polite, constructive, and mindful of the project's goals. + +on: + schedule: every 48 hours + workflow_dispatch: + slash_command: + name: repo-assist + reaction: "eyes" + +timeout-minutes: 60 + +permissions: read-all + +network: + allowed: + - defaults + - dotnet + - node + - python + - rust + - java + +checkout: + fetch: ["*"] # fetch all remote branches to allow working on PR branches + fetch-depth: 0 # fetch full history + +tools: + web-fetch: + github: + toolsets: [all] + min-integrity: none # This workflow is allowed to examine and comment on any issues or PRs + bash: true + repo-memory: true + +safe-outputs: + messages: + footer: "> Generated by 🌈 {workflow_name}, see [workflow run]({run_url}). [Learn more](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md)." + run-started: "{workflow_name} is processing {event_type}, see [workflow run]({run_url})..." + run-success: "✓ {workflow_name} completed successfully, see [workflow run]({run_url})." + run-failure: "✗ {workflow_name} encountered {status}, see [workflow run]({run_url})." + add-comment: + max: 10 + target: "*" + hide-older-comments: true + create-pull-request: + draft: true + title-prefix: "[Repo Assist] " + labels: [automation, repo-assist] + protected-files: fallback-to-issue + max: 4 + push-to-pull-request-branch: + target: "*" + title-prefix: "[Repo Assist] " + max: 4 + protected-files: fallback-to-issue + create-issue: + title-prefix: "[Repo Assist] " + labels: [automation, repo-assist] + max: 4 + update-issue: + target: "*" + title-prefix: "[Repo Assist] " + max: 1 + add-labels: + allowed: [bug, enhancement, "help wanted", "good first issue", "spam", "off topic", documentation, question, duplicate, wontfix, "needs triage", "needs investigation", "breaking change", performance, security, refactor] + max: 30 + target: "*" + remove-labels: + allowed: [bug, enhancement, "help wanted", "good first issue", "spam", "off topic", documentation, question, duplicate, wontfix, "needs triage", "needs investigation", "breaking change", performance, security, refactor] + max: 5 + target: "*" + +steps: + - name: Fetch repo data for task weighting + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p /tmp/gh-aw + + # Fetch open issues with labels (up to 500) + gh issue list --state open --limit 500 --json number,labels > /tmp/gh-aw/issues.json + + # Fetch open PRs with titles (up to 200) + gh pr list --state open --limit 200 --json number,title > /tmp/gh-aw/prs.json + + # Compute task weights and select two tasks for this run + python3 - << 'EOF' + import json, random, os + + with open('/tmp/gh-aw/issues.json') as f: + issues = json.load(f) + with open('/tmp/gh-aw/prs.json') as f: + prs = json.load(f) + + open_issues = len(issues) + unlabelled = sum(1 for i in issues if not i.get('labels')) + repo_assist_prs = sum(1 for p in prs if p['title'].startswith('[Repo Assist]')) + other_prs = sum(1 for p in prs if not p['title'].startswith('[Repo Assist]')) + + task_names = { + 1: 'Issue Labelling', + 2: 'Issue Investigation and Comment', + 3: 'Issue Investigation and Fix', + 4: 'Engineering Investments', + 5: 'Coding Improvements', + 6: 'Maintain Repo Assist PRs', + 7: 'Stale PR Nudges', + 8: 'Performance Improvements', + 9: 'Testing Improvements', + 10: 'Take the Repository Forward', + } + + weights = { + 1: 1 + 3 * unlabelled, + 2: 3 + 1 * open_issues, + 3: 3 + 0.7 * open_issues, + 4: 5 + 0.2 * open_issues, + 5: 5 + 0.1 * open_issues, + 6: float(repo_assist_prs), + 7: 0.1 * other_prs, + 8: 3 + 0.05 * open_issues, + 9: 3 + 0.05 * open_issues, + 10: 3 + 0.05 * open_issues, + } + + # Seed with run ID for reproducibility within a run + run_id = int(os.environ.get('GITHUB_RUN_ID', '0')) + rng = random.Random(run_id) + + task_ids = list(weights.keys()) + task_weights = [weights[t] for t in task_ids] + + # Weighted sample without replacement (pick 2 distinct tasks) + chosen, seen = [], set() + for t in rng.choices(task_ids, weights=task_weights, k=30): + if t not in seen: + seen.add(t) + chosen.append(t) + if len(chosen) == 2: + break + + print('=== Repo Assist Task Selection ===') + print(f'Open issues : {open_issues}') + print(f'Unlabelled issues : {unlabelled}') + print(f'Repo Assist PRs : {repo_assist_prs}') + print(f'Other open PRs : {other_prs}') + print() + print('Task weights:') + for t, w in weights.items(): + tag = ' <-- SELECTED' if t in chosen else '' + print(f' Task {t:2d} ({task_names[t]}): weight {w:6.1f}{tag}') + print() + print(f'Selected tasks for this run: Task {chosen[0]} ({task_names[chosen[0]]}) and Task {chosen[1]} ({task_names[chosen[1]]})') + + result = { + 'open_issues': open_issues, 'unlabelled_issues': unlabelled, + 'repo_assist_prs': repo_assist_prs, 'other_prs': other_prs, + 'task_names': task_names, + 'weights': {str(k): round(v, 2) for k, v in weights.items()}, + 'selected_tasks': chosen, + } + with open('/tmp/gh-aw/task_selection.json', 'w') as f: + json.dump(result, f, indent=2) + EOF + +source: githubnext/agentics/workflows/repo-assist.md@96b9d4c39aa22359c0b38265927eadb31dcf4e2a +--- + +# Repo Assist + +## Command Mode + +Take heed of **instructions**: "${{ steps.sanitized.outputs.text }}" + +If these are non-empty (not ""), then you have been triggered via `/repo-assist `. Follow the user's instructions instead of the normal scheduled workflow. Focus exclusively on those instructions. Apply all the same guidelines (read AGENTS.md, run formatters/linters/tests, be polite, use AI disclosure). Skip the weighted task selection and Task 11 reporting, and instead directly do what the user requested. If no specific instructions were provided (empty or blank), proceed with the normal scheduled workflow below. + +Then exit - do not run the normal workflow after completing the instructions. + +## Non-Command Mode + +You are Repo Assist for `${{ github.repository }}`. Your job is to support human contributors, help onboard newcomers, identify improvements, and fix bugs by creating pull requests. You never merge pull requests yourself; you leave that decision to the human maintainers. + +Always be: + +- **Polite and encouraging**: Every contributor deserves respect. Use warm, inclusive language. +- **Concise**: Keep comments focused and actionable. Avoid walls of text. +- **Mindful of project values**: Prioritize **stability**, **correctness**, and **minimal dependencies**. Do not introduce new dependencies without clear justification. +- **Transparent about your nature**: Always clearly identify yourself as Repo Assist, an automated AI assistant. Never pretend to be a human maintainer. +- **Restrained**: When in doubt, do nothing. It is always better to stay silent than to post a redundant, unhelpful, or spammy comment. Human maintainers' attention is precious - do not waste it. + +## Memory + +Use persistent repo memory to track: + +- issues already commented on (with timestamps to detect new human activity) +- fix attempts and outcomes, improvement ideas already submitted, a short to-do list +- a **backlog cursor** so each run continues where the previous one left off +- previously checked off items (checked off by maintainer) in the Monthly Activity Summary to maintain an accurate pending actions list for maintainers + +Read memory at the **start** of every run; update it at the **end**. + +**Important**: Memory may not be 100% accurate. Issues may have been created, closed, or commented on; PRs may have been created, merged, commented on, or closed since the last run. Always verify memory against current repository state — reviewing recent activity since your last run is wise before acting on stale assumptions. + +**Memory backlog tracking**: Your memory may contain notes about issues or PRs that still need attention (e.g., "issues #384, #336 have labels but no comments"). These are **action items for you**, not just informational notes. Each run, check your memory's `notes` field and other tracking fields for any explicitly flagged backlog work, and prioritise acting on it. + +## Workflow + +Each run, the deterministic pre-step collects live repo data (open issue count, unlabelled issue count, open Repo Assist PRs, other open PRs), computes a **weighted probability** for each task, and selects **two tasks** for this run using a seeded random draw. The weights and selected tasks are printed in the workflow logs. You will find the selection in `/tmp/gh-aw/task_selection.json`. + +**Read the task selection**: at the start of your run, read `/tmp/gh-aw/task_selection.json` and confirm the two selected tasks in your opening reasoning. Execute **those two tasks** (plus the mandatory Task 11). If there's really nothing to do for a selected task, do not force yourself to do it - try any other different task instead that looks most useful. + +The weighting scheme naturally adapts to repo state: + +- When unlabelled issues pile up, Task 1 (labelling) dominates. +- When there are many open issues, Tasks 2 and 3 (commenting and fixing) get more weight. +- As the backlog clears, Tasks 4–10 (engineering, improvements, nudges, forward progress) draw more evenly. + +**Repeat-run mode**: When invoked via `gh aw run repo-assist --repeat`, runs occur every 5–10 minutes. Each run is independent — do not skip a run. Always check memory to avoid duplicate work across runs. + +**Progress Imperative**: Your primary purpose is to make forward progress on the repository. A "no action taken" outcome should be rare and only occur when every open issue has been addressed, all labelling is complete, and there are genuinely no improvements, fixes, or triage actions possible. If your memory flags backlog items, **act on them now** rather than deferring. + +Always do Task 11 (Update Monthly Activity Summary Issue) every run. In all comments and PR descriptions, identify yourself as "Repo Assist". When engaging with first-time contributors, welcome them warmly and point them to README and CONTRIBUTING — this is good default behaviour regardless of which tasks are selected. + +### Task 1: Issue Labelling + +Process as many unlabelled issues and PRs as possible each run. Resume from memory's backlog cursor. + +For each item, apply the best-fitting labels from: `bug`, `enhancement`, `help wanted`, `good first issue`, `documentation`, `question`, `duplicate`, `wontfix`, `spam`, `off topic`, `needs triage`, `needs investigation`, `breaking change`, `performance`, `security`, `refactor`. Remove misapplied labels. Apply multiple where appropriate; skip any you're not confident about. After labelling, post a brief comment if you have something genuinely useful to add. + +Update memory with labels applied and cursor position. + +### Task 2: Issue Investigation and Comment + +1. List open issues sorted by creation date ascending (oldest first). Resume from your memory's backlog cursor; reset when you reach the end. +2. **Prioritise issues that have never received a Repo Assist comment.** Read the issue comments and check memory's `comments_made` field. Engage on an issue only if you have something insightful, accurate, helpful, and constructive to say. Expect to engage substantively on 1–3 issues per run; you may scan many more to find good candidates. Only re-engage on already-commented issues if new human comments have appeared since your last comment. +3. Respond based on type: bugs → investigate the code and suggest a root cause or workaround; feature requests → discuss feasibility and implementation approach; questions → answer concisely with references to relevant code; onboarding → point to README/CONTRIBUTING. Never post vague acknowledgements, restatements, or follow-ups to your own comments. +4. Begin every comment with: `🤖 *This is an automated response from Repo Assist.*` +5. Update memory with comments made and the new cursor position. + +### Task 3: Issue Investigation and Fix + +**Only attempt fixes you are confident about.** It is fine to work on issues you have previously commented on. + +1. Review issues labelled `bug`, `help wanted`, or `good first issue`, plus any identified as fixable during investigation. +2. For each fixable issue: + a. Check memory — skip if you've already tried and the attempt is still open. Never create duplicate PRs. + b. Create a fresh branch off the default branch of the repository: `repo-assist/fix-issue--`. + c. Implement a minimal, surgical fix. Do not refactor unrelated code. + d. **Build and test (required)**: do not create a PR if the build fails or tests fail due to your changes. If tests fail due to infrastructure, create the PR but document it. + e. Add a test for the bug if feasible; re-run tests. + f. Create a draft PR with: AI disclosure, `Closes #N`, root cause, fix rationale, trade-offs, and a Test Status section showing build/test outcome. + g. Post a single brief comment on the issue linking to the PR. +3. Update memory with fix attempts and outcomes. + +### Task 4: Engineering Investments + +Improve the engineering foundations of the repository. Consider: + +- **Dependency updates**: Check for outdated dependencies. Prefer minor/patch updates; propose major bumps only with clear benefit. **Bundle Dependabot PRs**: If multiple open Dependabot PRs exist, create a single bundled PR applying all compatible updates. Reference the original PRs so maintainers can close them after merging. +- **CI improvements**: Speed up CI pipelines, fix flaky tests, improve caching, upgrade actions. +- **Tooling and SDK versions**: Update runtime versions, linters, formatters. +- **Build system**: Simplify or modernise the build configuration. + +For any change: create a fresh branch `repo-assist/eng--`, implement the change, build and test, then create a draft PR with AI disclosure and Test Status section. Update memory with what was checked and when. + +### Task 5: Coding Improvements + +Study the codebase and make clearly beneficial, low-risk improvements. **Be highly selective — only propose changes with obvious value.** + +Good candidates: code clarity and readability, removing dead code, API usability, documentation gaps, reducing duplication. + +Check memory for already-submitted ideas; do not re-propose them. Create a fresh branch `repo-assist/improve-` off the default branch of the repository, implement the improvement, build and test (same requirements as Task 3), then create a draft PR with AI disclosure, rationale, and Test Status section. If not ready to implement, file an issue instead. Update memory. + +### Task 6: Maintain Repo Assist PRs + +1. List all open PRs with the `[Repo Assist]` title prefix. +2. For each PR: fix CI failures caused by your changes by pushing updates; resolve merge conflicts. If you've retried multiple times without success, comment and leave for human review. +3. Do not push updates for infrastructure-only failures — comment instead. +4. Update memory. + +### Task 7: Stale PR Nudges + +1. List open non-Repo-Assist PRs not updated in 14+ days. +2. For each (check memory — skip if already nudged): if the PR is waiting on the author, post a single polite comment asking if they need help or want to hand off. Do not comment if the PR is waiting on a maintainer. +3. **Maximum 3 nudges per run.** Update memory. + +### Task 8: Performance Improvements + +Identify and implement meaningful performance improvements. Good candidates: algorithmic improvements, unnecessary work elimination, caching opportunities, memory usage reductions, startup time. Only propose changes with a clear, measurable benefit. Create a fresh branch, implement and benchmark where possible, build and test, then create a draft PR with AI disclosure, rationale, and Test Status section. Update memory. + +### Task 9: Testing Improvements + +Improve the quality and coverage of the test suite. Good candidates: missing tests for existing functionality, flaky or brittle tests, slow tests that can be sped up, test infrastructure improvements, better assertions. Avoid adding low-value tests just to inflate coverage. Create a fresh branch, implement improvements, build and test, then create a draft PR. Update memory. + +### Task 10: Take the Repository Forward + +Proactively move the repository forward. Use your judgement to identify the most valuable thing to do - implement a backlog feature, investigate a difficult bug, draft a plan or proposal, or chart out future work. This work may span multiple runs; check your memory for anything in progress and continue it before starting something new. Record progress and next steps in memory at the end of each run. + +### Task 11: Update Monthly Activity Summary Issue (ALWAYS DO THIS TASK IN ADDITION TO OTHERS) + +Maintain a single open issue titled `[Repo Assist] Monthly Activity {YYYY}-{MM}` as a rolling summary of all Repo Assist activity for the current month. + +1. Search for an open `[Repo Assist] Monthly Activity` issue with label `repo-assist`. If it's for the current month, update it. If for a previous month, close it and create a new one. Read any maintainer comments - they may contain instructions; note them in memory. +2. **Issue body format** - use **exactly** this structure: + + ```markdown + 🤖 *Repo Assist here - I'm an automated AI assistant for this repository.* + + ## Activity for + + ## Suggested Actions for Maintainer + + **Comprehensive list** of all pending actions requiring maintainer attention (excludes items already actioned and checked off). + - Reread the issue you're updating before you update it - there may be new checkbox adjustments since your last update that require you to adjust the suggested actions. + - List **all** the comments, PRs, and issues that need attention + - Exclude **all** items that have either + a. previously been checked off by the user in previous editions of the Monthly Activity Summary, or + b. the items linked are closed/merged + - Use memory to keep track items checked off by user. + - Be concise - one line per item., repeating the format lines as necessary: + + * [ ] **Review PR** #: - [Review]() + * [ ] **Check comment** #: Repo Assist commented - verify guidance is helpful - [View]() + * [ ] **Merge PR** #: - [Review]() + * [ ] **Close issue** #: - [View]() + * [ ] **Close PR** #: - [View]() + * [ ] **Define goal**: - [Related issue]() + + *(If no actions needed, state "No suggested actions at this time.")* + + ## Future Work for Repo Assist + + {Very briefly list future work for Repo Assist} + + *(If nothing pending, skip this section.)* + + ## Run History + + ### - [Run](/actions/runs/>) + - 💬 Commented on #: + - 🔧 Created PR #: + - 🏷️ Labelled # with ` - new() = DummyTaskFactory(10_000L<µs>, 30_000L<µs>) + new() = DummyTaskFactory(1_000L<µs>, 5_000L<µs>) /// /// Creates dummy tasks with a randomized delay and a mutable state, @@ -164,8 +164,9 @@ module TestUtils = >> TaskSeq.toArrayAsync >> Task.map (String >> should equal expected) - /// Delays (no spin-wait!) between 20 and 70ms, assuming a 15.6ms resolution clock - let longDelay () = task { do! Task.Delay(Random().Next(20, 70)) } + /// Waits using a real timer-based async delay (not spin-wait), causing an OS-level async yield point. + /// On Windows, Task.Delay has ~15ms timer resolution, so this actually waits ~15ms. + let longDelay () = task { do! Task.Delay 1 } /// Spin-waits, occasionally normal delay, between 50µs - 18,000µs let microDelay () = task { do! DelayHelper.delayTask 50L<µs> 18_000L<µs> (fun _ -> ()) } @@ -262,11 +263,7 @@ module TestUtils = /// properly, sequentially execute a chain of tasks in a non-blocking, non-overlapping way. let joinWithContinuation tasks = let simple (t: unit -> Task<_>) (source: unit -> Task<_>) : unit -> Task<_> = - fun () -> - source() - .ContinueWith((fun (_: Task) -> t ()), TaskContinuationOptions.OnlyOnRanToCompletion) - .Unwrap() - :?> Task<_> + fun () -> source().ContinueWith((fun (_: Task) -> t ()), TaskContinuationOptions.OnlyOnRanToCompletion).Unwrap() :?> Task<_> let rec combine acc (tasks: (unit -> Task<_>) list) = match tasks with @@ -324,8 +321,8 @@ module TestUtils = yield x } - /// Create a bunch of dummy tasks, each lasting between 10-30ms with spin-wait delays. - let sideEffectTaskSeq = sideEffectTaskSeqMicro 10_000L<µs> 30_000L<µs> + /// Create a bunch of dummy tasks, each lasting between 1-5ms with spin-wait delays. + let sideEffectTaskSeq = sideEffectTaskSeqMicro 1_000L<µs> 5_000L<µs> /// Returns any of a set of variants that each create an empty sequence in a creative way. /// Please extend this with more cases. @@ -428,7 +425,7 @@ module TestUtils = | SeqImmutable.AsyncYielded -> // by returning the 'side effect seq' from the closure of the CE, // the side-effect will NOT execute again - taskSeq { yield! sideEffectTaskSeqMicro 15_000L<µs> 50_000L<µs> 10 } + taskSeq { yield! sideEffectTaskSeqMicro 1_000L<µs> 5_000L<µs> 10 } | SeqImmutable.AsyncYielded_Nested -> // let's deeply nest the sequence, which should not cause extra side effects being executed. taskSeq { @@ -442,7 +439,7 @@ module TestUtils = yield! taskSeq { // by returning the 'side effect seq' from the closure of the CE, // the side-effect will NOT execute again - yield! sideEffectTaskSeqMicro 15_000L<µs> 50_000L<µs> 10 + yield! sideEffectTaskSeqMicro 1_000L<µs> 5_000L<µs> 10 } } } @@ -520,12 +517,12 @@ module TestUtils = // delay just enough with a spin-wait to occasionally cause a thread-yield | SeqWithSideEffect.ThreadSpinWait -> sideEffectTaskSeqMicro 50L<µs> 5_000L<µs> 10 - | SeqWithSideEffect.AsyncYielded -> sideEffectTaskSeqMicro 15_000L<µs> 50_000L<µs> 10 + | SeqWithSideEffect.AsyncYielded -> sideEffectTaskSeqMicro 1_000L<µs> 5_000L<µs> 10 | SeqWithSideEffect.AsyncYielded_Nested -> // let's deeply nest the sequence, which should not cause extra side effects being executed. // NOTE: this list of tasks must be defined OUTSIDE the scope, otherwise, mutability on 2nd // iteration will not kick in! - let nestedTaskSeq = sideEffectTaskSeqMicro 15_000L<µs> 50_000L<µs> 10 + let nestedTaskSeq = sideEffectTaskSeqMicro 1_000L<µs> 5_000L<µs> 10 taskSeq { yield! taskSeq { diff --git a/src/FSharp.Control.TaskSeq.Test/Utils.Tests.fs b/src/FSharp.Control.TaskSeq.Test/Utils.Tests.fs new file mode 100644 index 00000000..c89765c5 --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/Utils.Tests.fs @@ -0,0 +1,116 @@ +module TaskSeq.Tests.Utils + +open System +open System.Threading.Tasks +open Xunit +open FsUnit.Xunit + +open FSharp.Control + + +module AsyncBind = + [] + let ``Async.bind awaits the async and passes the value to the binder`` () = + let result = + async { return 21 } + |> Async.bind (fun n -> async { return n * 2 }) + |> Async.RunSynchronously + + result |> should equal 42 + + [] + let ``Async.bind propagates exceptions from the source async`` () = + let run () = + async { return raise (InvalidOperationException "source error") } + |> Async.bind (fun (_: int) -> async { return 0 }) + |> Async.RunSynchronously + + (fun () -> run () |> ignore) + |> should throw typeof + + [] + let ``Async.bind propagates exceptions from the binder`` () = + let run () = + async { return 1 } + |> Async.bind (fun _ -> async { return raise (InvalidOperationException "binder error") }) + |> Async.RunSynchronously + + (fun () -> run () |> ignore) + |> should throw typeof + + [] + let ``Async.bind chains correctly`` () = + let result = + async { return 1 } + |> Async.bind (fun n -> async { return n + 10 }) + |> Async.bind (fun n -> async { return n + 100 }) + |> Async.RunSynchronously + + result |> should equal 111 + + [] + let ``Async.bind passes the unwrapped value, not the Async wrapper`` () = + // This test specifically verifies the bug fix: binder receives 'T, not Async<'T> + let mutable receivedType = typeof + + async { return 42 } + |> Async.bind (fun (n: int) -> + receivedType <- n.GetType() + async { return () }) + |> Async.RunSynchronously + + receivedType |> should equal typeof + + +module TaskBind = + [] + let ``Task.bind awaits the task and passes the value to the binder`` () = task { + let result = + task { return 21 } + |> Task.bind (fun n -> task { return n * 2 }) + + let! v = result + v |> should equal 42 + } + + [] + let ``Task.bind chains correctly`` () = task { + let result = + task { return 1 } + |> Task.bind (fun n -> task { return n + 10 }) + |> Task.bind (fun n -> task { return n + 100 }) + + let! v = result + v |> should equal 111 + } + + +module AsyncMap = + [] + let ``Async.map transforms the result`` () = + let result = + async { return 21 } + |> Async.map (fun n -> n * 2) + |> Async.RunSynchronously + + result |> should equal 42 + + [] + let ``Async.map chains correctly`` () = + let result = + async { return 1 } + |> Async.map (fun n -> n + 10) + |> Async.map (fun n -> n + 100) + |> Async.RunSynchronously + + result |> should equal 111 + + +module TaskMap = + [] + let ``Task.map transforms the result`` () = task { + let result = task { return 21 } |> Task.map (fun n -> n * 2) + + let! v = result + v |> should equal 42 + } diff --git a/src/FSharp.Control.TaskSeq/AsyncExtensions.fs b/src/FSharp.Control.TaskSeq/AsyncExtensions.fs index 261e3437..d257d7de 100644 --- a/src/FSharp.Control.TaskSeq/AsyncExtensions.fs +++ b/src/FSharp.Control.TaskSeq/AsyncExtensions.fs @@ -3,10 +3,30 @@ namespace FSharp.Control [] module AsyncExtensions = + // Awaits a Task without wrapping exceptions in AggregateException. + // Async.AwaitTask wraps task exceptions in AggregateException, which breaks try/catch + // blocks in async {} expressions that expect the original exception type. + // See: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/129 + let private awaitTaskCorrect (task: System.Threading.Tasks.Task) : Async = + Async.FromContinuations(fun (cont, econt, ccont) -> + task.ContinueWith(fun (t: System.Threading.Tasks.Task) -> + if t.IsFaulted then + let exn = t.Exception + + if exn.InnerExceptions.Count = 1 then + econt exn.InnerExceptions.[0] + else + econt exn + elif t.IsCanceled then + ccont (System.OperationCanceledException "The operation was cancelled.") + else + cont ()) + |> ignore) + // Add asynchronous for loop to the 'async' computation builder type Microsoft.FSharp.Control.AsyncBuilder with member _.For(source: TaskSeq<'T>, action: 'T -> Async) = source |> TaskSeq.iterAsync (action >> Async.StartImmediateAsTask) - |> Async.AwaitTask + |> awaitTaskCorrect diff --git a/src/FSharp.Control.TaskSeq/CompatibilitySuppressions.xml b/src/FSharp.Control.TaskSeq/CompatibilitySuppressions.xml new file mode 100644 index 00000000..c0e8a357 --- /dev/null +++ b/src/FSharp.Control.TaskSeq/CompatibilitySuppressions.xml @@ -0,0 +1,32 @@ + + + + + CP0002 + M:FSharp.Control.Async.bind``2(Microsoft.FSharp.Core.FSharpFunc{Microsoft.FSharp.Control.FSharpAsync{``0},Microsoft.FSharp.Control.FSharpAsync{``1}},Microsoft.FSharp.Control.FSharpAsync{``0}) + lib/netstandard2.1/FSharp.Control.TaskSeq.dll + lib/netstandard2.1/FSharp.Control.TaskSeq.dll + true + + + CP0002 + M:FSharp.Control.LowPriority.TaskSeqBuilder#Bind``5(FSharp.Control.TaskSeqBuilder,``0,Microsoft.FSharp.Core.FSharpFunc{``1,Microsoft.FSharp.Core.CompilerServices.ResumableCode{FSharp.Control.TaskSeqStateMachineData{``2},Microsoft.FSharp.Core.Unit}}) + lib/netstandard2.1/FSharp.Control.TaskSeq.dll + lib/netstandard2.1/FSharp.Control.TaskSeq.dll + true + + + CP0002 + M:FSharp.Control.LowPriority.TaskSeqBuilder#Bind$W``5(Microsoft.FSharp.Core.FSharpFunc{``0,``3},Microsoft.FSharp.Core.FSharpFunc{``3,``1},Microsoft.FSharp.Core.FSharpFunc{``3,System.Boolean},FSharp.Control.TaskSeqBuilder,``0,Microsoft.FSharp.Core.FSharpFunc{``1,Microsoft.FSharp.Core.CompilerServices.ResumableCode{FSharp.Control.TaskSeqStateMachineData{``2},Microsoft.FSharp.Core.Unit}}) + lib/netstandard2.1/FSharp.Control.TaskSeq.dll + lib/netstandard2.1/FSharp.Control.TaskSeq.dll + true + + + CP0002 + M:FSharp.Control.TaskExtensions.TaskBuilder#For``2(Microsoft.FSharp.Control.TaskBuilder,System.Collections.Generic.IAsyncEnumerable{``0},Microsoft.FSharp.Core.FSharpFunc{``0,Microsoft.FSharp.Core.CompilerServices.ResumableCode{Microsoft.FSharp.Control.TaskStateMachineData{``1},Microsoft.FSharp.Core.Unit}}) + lib/netstandard2.1/FSharp.Control.TaskSeq.dll + lib/netstandard2.1/FSharp.Control.TaskSeq.dll + true + + \ No newline at end of file diff --git a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj index 297af81f..e3bae0c1 100644 --- a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj +++ b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj @@ -13,18 +13,25 @@ The 'taskSeq' computation expression adds support for awaitable asynchronous sequences with similar ease of use and performance to F#'s 'task' CE, with minimal overhead through ValueTask under the hood. TaskSeq brings 'seq' and 'task' together in a safe way. Generates optimized IL code through resumable state machines, and comes with a comprehensive set of functions in module 'TaskSeq'. See README for documentation and more info. - Copyright 2023 - https://github.com/fsprojects/FSharp.Control.TaskSeq + Copyright 2022-2024 + https://fsprojects.github.io/FSharp.Control.TaskSeq/ https://github.com/fsprojects/FSharp.Control.TaskSeq taskseq-icon.png ..\..\packages MIT False nuget-package-readme.md + https://github.com/fsprojects/FSharp.Control.TaskSeq/blob/main/LICENSE + https://github.com/fsprojects/FSharp.Control.TaskSeq/blob/main/release-notes.txt + img/logo.png $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../../release-notes.txt")) - taskseq;f#;computation expression;IAsyncEnumerable;task;async;asyncseq; + taskseq;f#;fsharp;asyncseq;seq;sequences;sequential;threading;computation expression;IAsyncEnumerable;task;async;iteration True snupkg + + true + + 0.4.0 @@ -57,7 +64,15 @@ Generates optimized IL code through resumable state machines, and comes with a c - - + + + + true + diff --git a/src/FSharp.Control.TaskSeq/TaskExtensions.fs b/src/FSharp.Control.TaskSeq/TaskExtensions.fs index 63a72d95..a811e8a4 100644 --- a/src/FSharp.Control.TaskSeq/TaskExtensions.fs +++ b/src/FSharp.Control.TaskSeq/TaskExtensions.fs @@ -14,8 +14,8 @@ open Microsoft.FSharp.Core.LanguagePrimitives.IntrinsicOperators [] module TaskExtensions = - // Add asynchronous for loop to the 'task' computation builder - type Microsoft.FSharp.Control.TaskBuilder with + // Add asynchronous for loop to the 'task' and 'backgroundTask' computation builders + type TaskBuilderBase with /// Used by `For`. F# currently doesn't support `while!`, so this cannot be called directly from the task CE /// This code is mostly a copy of TaskSeq.WhileAsync. diff --git a/src/FSharp.Control.TaskSeq/TaskExtensions.fsi b/src/FSharp.Control.TaskSeq/TaskExtensions.fsi index c99c6762..c723ee9f 100644 --- a/src/FSharp.Control.TaskSeq/TaskExtensions.fsi +++ b/src/FSharp.Control.TaskSeq/TaskExtensions.fsi @@ -5,7 +5,7 @@ namespace FSharp.Control [] module TaskExtensions = - type TaskBuilder with + type TaskBuilderBase with /// /// Inside , iterate over all values of a . diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index 710dadd8..d3a94f98 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -13,6 +13,107 @@ module TaskSeqExtensions = module TaskSeq = let empty<'T> = Internal.empty<'T> + let inline sum (source: TaskSeq< ^T >) : Task< ^T > = + if obj.ReferenceEquals(source, null) then + nullArg (nameof source) + + task { + use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None) + let mutable acc = Unchecked.defaultof< ^T> + + while! e.MoveNextAsync() do + acc <- acc + e.Current + + return acc + } + + let inline sumBy (projection: 'T -> ^U) (source: TaskSeq<'T>) : Task< ^U > = + if obj.ReferenceEquals(source, null) then + nullArg (nameof source) + + task { + use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None) + let mutable acc = Unchecked.defaultof< ^U> + + while! e.MoveNextAsync() do + acc <- acc + projection e.Current + + return acc + } + + let inline sumByAsync (projection: 'T -> Task< ^U >) (source: TaskSeq<'T>) : Task< ^U > = + if obj.ReferenceEquals(source, null) then + nullArg (nameof source) + + task { + use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None) + let mutable acc = Unchecked.defaultof< ^U> + + while! e.MoveNextAsync() do + let! value = projection e.Current + acc <- acc + value + + return acc + } + + let inline average (source: TaskSeq< ^T >) : Task< ^T > = + if obj.ReferenceEquals(source, null) then + nullArg (nameof source) + + task { + use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None) + let mutable acc = Unchecked.defaultof< ^T> + let mutable count = 0 + + while! e.MoveNextAsync() do + acc <- acc + e.Current + count <- count + 1 + + if count = 0 then + invalidArg (nameof source) "The input task sequence was empty." + + return LanguagePrimitives.DivideByInt acc count + } + + let inline averageBy (projection: 'T -> ^U) (source: TaskSeq<'T>) : Task< ^U > = + if obj.ReferenceEquals(source, null) then + nullArg (nameof source) + + task { + use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None) + let mutable acc = Unchecked.defaultof< ^U> + let mutable count = 0 + + while! e.MoveNextAsync() do + acc <- acc + projection e.Current + count <- count + 1 + + if count = 0 then + invalidArg (nameof source) "The input task sequence was empty." + + return LanguagePrimitives.DivideByInt acc count + } + + let inline averageByAsync (projection: 'T -> Task< ^U >) (source: TaskSeq<'T>) : Task< ^U > = + if obj.ReferenceEquals(source, null) then + nullArg (nameof source) + + task { + use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None) + let mutable acc = Unchecked.defaultof< ^U> + let mutable count = 0 + + while! e.MoveNextAsync() do + let! value = projection e.Current + acc <- acc + value + count <- count + 1 + + if count = 0 then + invalidArg (nameof source) "The input task sequence was empty." + + return LanguagePrimitives.DivideByInt acc count + } + [] type TaskSeq private () = @@ -22,6 +123,10 @@ type TaskSeq private () = // the 'private ()' ensure that a constructor is emitted, which is required by IL static member singleton(value: 'T) = Internal.singleton value + static member replicate count value = Internal.replicate count value + static member replicateInfinite value = Internal.replicateInfinite value + static member replicateInfiniteAsync computation = Internal.replicateInfiniteAsync computation + static member replicateUntilNoneAsync computation = Internal.replicateUntilNoneAsync computation static member isEmpty source = Internal.isEmpty source @@ -156,6 +261,13 @@ type TaskSeq private () = yield c } + static member withCancellation (cancellationToken: CancellationToken) (source: TaskSeq<'T>) = + Internal.checkNonNull (nameof source) source + + { new IAsyncEnumerable<'T> with + member _.GetAsyncEnumerator(_ct) = source.GetAsyncEnumerator(cancellationToken) + } + // // Utility functions // @@ -166,6 +278,7 @@ type TaskSeq private () = static member minBy projection source = Internal.maxMinBy (>) projection source static member maxByAsync projection source = Internal.maxMinByAsync (<) projection source // looks like 'less than', is 'greater than' static member minByAsync projection source = Internal.maxMinByAsync (>) projection source + static member length source = Internal.lengthBy None source static member lengthOrMax max source = Internal.lengthBeforeMax max source static member lengthBy predicate source = Internal.lengthBy (Some(Predicate predicate)) source @@ -175,6 +288,9 @@ type TaskSeq private () = static member initAsync count initializer = Internal.init (Some count) (InitActionAsync initializer) static member initInfiniteAsync initializer = Internal.init None (InitActionAsync initializer) + static member unfold generator state = Internal.unfold generator state + static member unfoldAsync generator state = Internal.unfoldAsync generator state + static member delay(generator: unit -> TaskSeq<'T>) = { new IAsyncEnumerable<'T> with member _.GetAsyncEnumerator(ct) = generator().GetAsyncEnumerator(ct) @@ -288,6 +404,10 @@ type TaskSeq private () = Internal.tryLast source |> Task.map (Option.defaultWith Internal.raiseEmptySeq) + static member firstOrDefault defaultValue source = Internal.firstOrDefault defaultValue source + static member lastOrDefault defaultValue source = Internal.lastOrDefault defaultValue source + static member splitAt count source = Internal.splitAt count source + static member tryTail source = Internal.tryTail source static member tail source = @@ -321,7 +441,9 @@ type TaskSeq private () = } static member choose chooser source = Internal.choose (TryPick chooser) source + static member chooseV chooser source = Internal.chooseV (TryPickV chooser) source static member chooseAsync chooser source = Internal.choose (TryPickAsync chooser) source + static member chooseVAsync chooser source = Internal.chooseV (TryPickVAsync chooser) source static member filter predicate source = Internal.filter (Predicate predicate) source static member filterAsync predicate source = Internal.filter (PredicateAsync predicate) source @@ -358,20 +480,27 @@ type TaskSeq private () = static member except itemsToExclude source = Internal.except itemsToExclude source static member exceptOfSeq itemsToExclude source = Internal.exceptOfSeq itemsToExclude source + static member distinct source = Internal.distinct source + static member distinctBy projection source = Internal.distinctBy projection source + static member distinctByAsync projection source = Internal.distinctByAsync projection source + + static member distinctUntilChanged source = Internal.distinctUntilChanged source + static member distinctUntilChangedWith comparer source = Internal.distinctUntilChangedWith comparer source + static member distinctUntilChangedWithAsync comparer source = Internal.distinctUntilChangedWithAsync comparer source + static member pairwise source = Internal.pairwise source + static member chunkBySize chunkSize source = Internal.chunkBySize chunkSize source + static member chunkBy projection source = Internal.chunkBy projection source + static member chunkByAsync projection source = Internal.chunkByAsync projection source + static member windowed windowSize source = Internal.windowed windowSize source + static member forall predicate source = Internal.forall (Predicate predicate) source static member forallAsync predicate source = Internal.forall (PredicateAsync predicate) source - static member exists predicate source = - Internal.tryFind (Predicate predicate) source - |> Task.map Option.isSome + static member exists predicate source = Internal.exists (Predicate predicate) source - static member existsAsync predicate source = - Internal.tryFind (PredicateAsync predicate) source - |> Task.map Option.isSome + static member existsAsync predicate source = Internal.exists (PredicateAsync predicate) source - static member contains value source = - Internal.tryFind (Predicate((=) value)) source - |> Task.map Option.isSome + static member contains value source = Internal.contains value source static member pick chooser source = Internal.tryPick (TryPick chooser) source @@ -402,5 +531,31 @@ type TaskSeq private () = // static member zip source1 source2 = Internal.zip source1 source2 + static member zip3 source1 source2 source3 = Internal.zip3 source1 source2 source3 + static member zipWith mapping source1 source2 = Internal.zipWith mapping source1 source2 + static member zipWithAsync mapping source1 source2 = Internal.zipWithAsync mapping source1 source2 + static member zipWith3 mapping source1 source2 source3 = Internal.zipWith3 mapping source1 source2 source3 + static member zipWithAsync3 mapping source1 source2 source3 = Internal.zipWithAsync3 mapping source1 source2 source3 + static member compareWith comparer source1 source2 = Internal.compareWith comparer source1 source2 + static member compareWithAsync comparer source1 source2 = Internal.compareWithAsync comparer source1 source2 static member fold folder state source = Internal.fold (FolderAction folder) state source static member foldAsync folder state source = Internal.fold (AsyncFolderAction folder) state source + static member scan folder state source = Internal.scan (FolderAction folder) state source + static member scanAsync folder state source = Internal.scan (AsyncFolderAction folder) state source + static member reduce folder source = Internal.reduce (FolderAction folder) source + static member reduceAsync folder source = Internal.reduce (AsyncFolderAction folder) source + + // + // groupBy/countBy/partition + // + + static member groupBy projection source = Internal.groupBy (ProjectorAction projection) source + static member groupByAsync projection source = Internal.groupBy (AsyncProjectorAction projection) source + static member countBy projection source = Internal.countBy (ProjectorAction projection) source + static member countByAsync projection source = Internal.countBy (AsyncProjectorAction projection) source + static member partition predicate source = Internal.partition (Predicate predicate) source + static member partitionAsync predicate source = Internal.partition (PredicateAsync predicate) source + static member mapFold mapping state source = Internal.mapFold (MapFolderAction mapping) state source + static member mapFoldAsync mapping state source = Internal.mapFold (AsyncMapFolderAction mapping) state source + static member threadState folder state source = Internal.threadState folder state source + static member threadStateAsync folder state source = Internal.threadStateAsync folder state source diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index cb5eefe5..1caf8262 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -1,6 +1,7 @@ namespace FSharp.Control open System.Collections.Generic +open System.Threading open System.Threading.Tasks [] @@ -9,6 +10,90 @@ module TaskSeqExtensions = /// Initialize an empty task sequence. val empty<'T> : TaskSeq<'T> + /// + /// Returns the sum of all elements of the task sequence. The elements must support the + operator, + /// which is the case for all built-in numeric types. For sequences with a projection, use . + /// + /// + /// The input task sequence. + /// The sum of all elements in the sequence, starting from Unchecked.defaultof as zero. + /// Thrown when the input task sequence is null. + val inline sum: source: TaskSeq< ^T > -> Task< ^T > when ^T: (static member (+): ^T * ^T -> ^T) + + /// + /// Returns the sum of the results generated by applying the function to each element + /// of the task sequence. The result type must support the + operator, which is the case for all built-in numeric types. + /// If is asynchronous, consider using . + /// + /// + /// A function to transform items from the input sequence into summable values. + /// The input task sequence. + /// The sum of the projected values. + /// Thrown when the input task sequence is null. + val inline sumBy: + projection: ('T -> ^U) -> source: TaskSeq<'T> -> Task< ^U > when ^U: (static member (+): ^U * ^U -> ^U) + + /// + /// Returns the sum of the results generated by applying the asynchronous function to + /// each element of the task sequence. The result type must support the + operator, which is the case for all + /// built-in numeric types. + /// If is synchronous, consider using . + /// + /// + /// An async function to transform items from the input sequence into summable values. + /// The input task sequence. + /// The sum of the projected values. + /// Thrown when the input task sequence is null. + val inline sumByAsync: + projection: ('T -> Task< ^U >) -> source: TaskSeq<'T> -> Task< ^U > + when ^U: (static member (+): ^U * ^U -> ^U) + + /// + /// Returns the average of all elements of the task sequence. The elements must support the + operator + /// and DivideByInt, which is the case for all built-in F# floating-point types. + /// For sequences with a projection, consider using . + /// + /// + /// The input task sequence. + /// The average of the elements in the sequence. + /// Thrown when the input task sequence is null. + /// Thrown when the input task sequence is empty. + val inline average: + source: TaskSeq< ^T > -> Task< ^T > + when ^T: (static member (+): ^T * ^T -> ^T) and ^T: (static member DivideByInt: ^T * int -> ^T) + + /// + /// Returns the average of the results generated by applying the function to each element + /// of the task sequence. The result type must support the + operator and DivideByInt, which is the case + /// for all built-in F# floating-point types. + /// If is asynchronous, consider using . + /// + /// + /// A function to transform items from the input sequence into averageable values. + /// The input task sequence. + /// The average of the projected values. + /// Thrown when the input task sequence is null. + /// Thrown when the input task sequence is empty. + val inline averageBy: + projection: ('T -> ^U) -> source: TaskSeq<'T> -> Task< ^U > + when ^U: (static member (+): ^U * ^U -> ^U) and ^U: (static member DivideByInt: ^U * int -> ^U) + + /// + /// Returns the average of the results generated by applying the asynchronous function to + /// each element of the task sequence. The result type must support the + operator and DivideByInt, which + /// is the case for all built-in F# floating-point types. + /// If is synchronous, consider using . + /// + /// + /// An async function to transform items from the input sequence into averageable values. + /// The input task sequence. + /// The average of the projected values. + /// Thrown when the input task sequence is null. + /// Thrown when the input task sequence is empty. + val inline averageByAsync: + projection: ('T -> Task< ^U >) -> source: TaskSeq<'T> -> Task< ^U > + when ^U: (static member (+): ^U * ^U -> ^U) and ^U: (static member DivideByInt: ^U * int -> ^U) + [] type TaskSeq = @@ -17,13 +102,58 @@ type TaskSeq = /// /// /// The input item to use as the single item of the task sequence. + /// A task sequence containing exactly one element. static member singleton: value: 'T -> TaskSeq<'T> + /// + /// Creates a task sequence by replicating a total of times. + /// + /// + /// The number of times to replicate the value. + /// The value to replicate. + /// A task sequence containing copies of . + /// Thrown when is negative. + static member replicate: count: int -> value: 'T -> TaskSeq<'T> + + /// + /// Creates an infinite task sequence by repeating indefinitely. + /// The sequence never ends; use , , + /// or similar to bound consumption. + /// + /// + /// The value to repeat. + /// An infinite task sequence of . + static member replicateInfinite: value: 'T -> TaskSeq<'T> + + /// + /// Creates an infinite task sequence by repeatedly executing , + /// yielding each produced value indefinitely. + /// The sequence never ends; use , , + /// or similar to bound consumption. + /// If the computation is synchronous, consider using . + /// + /// + /// A function that produces the next value on each invocation. + /// An infinite task sequence of values produced by . + static member replicateInfiniteAsync: computation: (unit -> #Task<'T>) -> TaskSeq<'T> + + /// + /// Creates a task sequence by repeatedly executing until it returns + /// , yielding each value in order. + /// + /// + /// + /// A function that returns value to emit, or to end the sequence. + /// + /// A task sequence of values produced by until it returns . + static member replicateUntilNoneAsync: computation: (unit -> #Task<'T option>) -> TaskSeq<'T> + /// /// Returns if the task sequence contains no elements, otherwise. /// /// /// The input task sequence. + /// A task returning true if the sequence contains no elements; false otherwise. /// Thrown when the input task sequence is null. static member isEmpty: source: TaskSeq<'T> -> Task @@ -33,6 +163,7 @@ type TaskSeq = /// /// /// The input task sequence. + /// A task returning the number of elements in the sequence. /// Thrown when the input task sequence is null. static member length: source: TaskSeq<'T> -> Task @@ -44,6 +175,7 @@ type TaskSeq = /// /// Limit at which to stop evaluating source items for finding the length. /// The input task sequence. + /// A task returning the actual length of the sequence, or if the sequence is longer than . /// Thrown when the input task sequence is null. static member lengthOrMax: max: int -> source: TaskSeq<'T> -> Task @@ -55,6 +187,7 @@ type TaskSeq = /// /// A function to test whether an item in the input sequence should be included in the count. /// The input task sequence. + /// A task returning the number of elements for which returns true. /// Thrown when the input task sequence is null. static member lengthBy: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task @@ -66,6 +199,7 @@ type TaskSeq = /// /// A function to test whether an item in the input sequence should be included in the count. /// The input task sequence. + /// A task returning the number of elements for which returns true. /// Thrown when the input task sequence is null. static member lengthByAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> Task @@ -211,6 +345,34 @@ type TaskSeq = /// The resulting task sequence. static member initInfiniteAsync: initializer: (int -> #Task<'T>) -> TaskSeq<'T> + /// + /// Returns a task sequence generated by applying the generator function to a state value, until it returns None. + /// Each call to returns either None, which terminates the sequence, or + /// Some(element, newState), which yields and updates the state for the next call. + /// Unlike , the number of elements need not be known in advance. + /// If the generator function is asynchronous, consider using . + /// + /// + /// A function that takes the current state and returns either None to terminate, + /// or Some(element, newState) to yield an element and continue with a new state. + /// The initial state value. + /// The resulting task sequence. + static member unfold: generator: ('State -> ('T * 'State) option) -> state: 'State -> TaskSeq<'T> + + /// + /// Returns a task sequence generated by applying the asynchronous generator function to a state value, until it + /// returns None. Each call to returns either None, which terminates the + /// sequence, or Some(element, newState), which yields and updates the state. + /// Unlike , the number of elements need not be known in advance. + /// If the generator function is synchronous, consider using . + /// + /// + /// An async function that takes the current state and returns either None to terminate, + /// or Some(element, newState) to yield an element and continue with a new state. + /// The initial state value. + /// The resulting task sequence. + static member unfoldAsync: generator: ('State -> Task<('T * 'State) option>) -> state: 'State -> TaskSeq<'T> + /// /// Combines the given task sequence of task sequences and concatenates them end-to-end, to form a /// new flattened, single task sequence, like . Each task sequence is @@ -274,7 +436,7 @@ type TaskSeq = static member append: source1: TaskSeq<'T> -> source2: TaskSeq<'T> -> TaskSeq<'T> /// - /// Concatenates a task sequence with a non-async F# in + /// Concatenates a task sequence with a (non-async) F# in /// and returns a single task sequence. /// /// @@ -285,7 +447,7 @@ type TaskSeq = static member appendSeq: source1: TaskSeq<'T> -> source2: seq<'T> -> TaskSeq<'T> /// - /// Concatenates a non-async F# in with a task sequence in + /// Concatenates a (non-async) F# in with a task sequence in /// and returns a single task sequence. /// /// @@ -480,6 +642,23 @@ type TaskSeq = /// Thrown when the input sequence is null. static member ofAsyncArray: source: Async<'T> array -> TaskSeq<'T> + /// + /// Returns a task sequence that, when iterated, passes the given to the + /// underlying . This is the equivalent of calling + /// .WithCancellation(cancellationToken) on an . + /// + /// + /// The supplied to this function overrides any token that would otherwise + /// be passed to the enumerator. This is useful when consuming sequences from libraries such as Entity Framework, + /// which accept a through GetAsyncEnumerator. + /// + /// + /// The cancellation token to pass to GetAsyncEnumerator. + /// The input task sequence. + /// A task sequence that uses the given when iterated. + /// Thrown when the input task sequence is null. + static member withCancellation: cancellationToken: CancellationToken -> source: TaskSeq<'T> -> TaskSeq<'T> + /// /// Views each item in the input task sequence as , boxing value types. /// @@ -753,6 +932,28 @@ type TaskSeq = /// Thrown when the task sequence is empty. static member last: source: TaskSeq<'T> -> Task<'T> + /// + /// Returns the first element of the input task sequence given by , + /// or if the sequence is empty. + /// + /// + /// The value to return when the source sequence is empty. + /// The input task sequence. + /// The first element of the task sequence, or if empty. + /// Thrown when the input task sequence is null. + static member firstOrDefault: defaultValue: 'T -> source: TaskSeq<'T> -> Task<'T> + + /// + /// Returns the last element of the input task sequence given by , + /// or if the sequence is empty. + /// + /// + /// The value to return when the source sequence is empty. + /// The input task sequence. + /// The last element of the task sequence, or if empty. + /// Thrown when the input task sequence is null. + static member lastOrDefault: defaultValue: 'T -> source: TaskSeq<'T> -> Task<'T> + /// /// Returns the nth element of the input task sequence given by , /// or if the sequence does not contain enough elements. @@ -810,6 +1011,18 @@ type TaskSeq = /// Thrown when the input task sequence is null. static member choose: chooser: ('T -> 'U option) -> source: TaskSeq<'T> -> TaskSeq<'U> + /// + /// Applies the given function to each element of the task sequence. Returns + /// a sequence comprised of the results where the function returns . + /// If is asynchronous, consider using . + /// + /// + /// A function to transform items of type into value options of type . + /// The input task sequence. + /// The resulting task sequence. + /// Thrown when the input task sequence is null. + static member chooseV: chooser: ('T -> 'U voption) -> source: TaskSeq<'T> -> TaskSeq<'U> + /// /// Applies the given asynchronous function to each element of the task sequence. /// Returns a sequence comprised of the results where the function returns a result @@ -823,6 +1036,19 @@ type TaskSeq = /// Thrown when the input task sequence is null. static member chooseAsync: chooser: ('T -> #Task<'U option>) -> source: TaskSeq<'T> -> TaskSeq<'U> + /// + /// Applies the given asynchronous function to each element of the task sequence. + /// Returns a sequence comprised of the results where the function returns a result + /// of . + /// If is synchronous, consider using . + /// + /// + /// An asynchronous function to transform items of type into value options of type . + /// The input task sequence. + /// The resulting task sequence. + /// Thrown when the input task sequence is null. + static member chooseVAsync: chooser: ('T -> #Task<'U voption>) -> source: TaskSeq<'T> -> TaskSeq<'U> + /// /// Returns a new task sequence containing only the elements of the collection /// for which the given function returns . @@ -961,6 +1187,27 @@ type TaskSeq = /// Thrown when is less than zero. static member truncate: count: int -> source: TaskSeq<'T> -> TaskSeq<'T> + /// + /// Splits the task sequence into a prefix array of at most elements and a task + /// sequence containing the remaining elements. The prefix is eagerly evaluated in a single pass; the + /// remaining sequence is lazy. If the source has fewer than elements, the prefix + /// array is shorter than and the remaining sequence is empty. + /// + /// + /// + /// The prefix array and the remaining task sequence share a single enumerator over . + /// For sequences backed by replayable data (arrays, lists, taskSeq builders, etc.) this is always safe. + /// For externally-managed resources (network streams, database cursors) the remaining sequence should be + /// consumed or disposed before any other operation on . + /// + /// + /// The maximum number of elements in the prefix. Must be non-negative. + /// The input task sequence. + /// A task returning a tuple (prefix, rest) where prefix is an array of the first elements and rest is the remaining task sequence. + /// Thrown when the input task sequence is null. + /// Thrown when is negative. + static member splitAt: count: int -> source: TaskSeq<'T> -> Task<'T[] * TaskSeq<'T>> + /// /// Returns a task sequence that, when iterated, yields elements of the underlying sequence while the /// given function returns , and then returns no further elements. @@ -1297,6 +1544,173 @@ type TaskSeq = /// Thrown when either of the two input task sequences is null. static member exceptOfSeq<'T when 'T: equality> : itemsToExclude: seq<'T> -> source: TaskSeq<'T> -> TaskSeq<'T> + /// + /// Returns a new task sequence that contains no duplicate entries, using generic hash and equality comparisons. + /// If an element occurs multiple times in the sequence, only the first occurrence is returned. + /// + /// + /// + /// This function iterates the whole sequence and buffers all unique elements in a hash set, so it should not + /// be used on potentially infinite sequences. + /// + /// + /// The input task sequence. + /// A sequence with duplicate elements removed. + /// + /// Thrown when the input task sequence is null. + static member distinct<'T when 'T: equality> : source: TaskSeq<'T> -> TaskSeq<'T> + + /// + /// Returns a new task sequence that contains no duplicate entries according to the generic hash and equality + /// comparisons on the keys returned by the given projection function. + /// If two elements have the same projected key, only the first occurrence is returned. + /// If the projection function is asynchronous, consider using . + /// + /// + /// + /// This function iterates the whole sequence and buffers all unique keys in a hash set, so it should not + /// be used on potentially infinite sequences. + /// + /// + /// A function that transforms each element to a key that is used for equality comparison. + /// The input task sequence. + /// A sequence with elements whose projected keys are distinct. + /// + /// Thrown when the input task sequence is null. + static member distinctBy<'T, 'Key when 'Key: equality> : + projection: ('T -> 'Key) -> source: TaskSeq<'T> -> TaskSeq<'T> + + /// + /// Returns a new task sequence that contains no duplicate entries according to the generic hash and equality + /// comparisons on the keys returned by the given asynchronous projection function. + /// If two elements have the same projected key, only the first occurrence is returned. + /// If the projection function is synchronous, consider using . + /// + /// + /// + /// This function iterates the whole sequence and buffers all unique keys in a hash set, so it should not + /// be used on potentially infinite sequences. + /// + /// + /// An asynchronous function that transforms each element to a key used for equality comparison. + /// The input task sequence. + /// A sequence with elements whose projected keys are distinct. + /// + /// Thrown when the input task sequence is null. + static member distinctByAsync: + projection: ('T -> #Task<'Key>) -> source: TaskSeq<'T> -> TaskSeq<'T> when 'Key: equality + + /// + /// Returns a new task sequence without consecutive duplicate elements. + /// + /// + /// The input task sequence whose consecutive duplicates will be removed. + /// A sequence without consecutive duplicates elements. + /// + /// Thrown when the input task sequences is null. + static member distinctUntilChanged<'T when 'T: equality> : source: TaskSeq<'T> -> TaskSeq<'T> + + /// + /// Returns a new task sequence without consecutive duplicate elements, using the supplied + /// to determine equality of consecutive elements. The comparer returns if two elements are + /// considered equal (and thus the second should be skipped). + /// + /// + /// A function that returns if two consecutive elements are equal. + /// The input task sequence whose consecutive duplicates will be removed. + /// A sequence without consecutive duplicate elements. + /// + /// Thrown when the input task sequence is null. + static member distinctUntilChangedWith: comparer: ('T -> 'T -> bool) -> source: TaskSeq<'T> -> TaskSeq<'T> + + /// + /// Returns a new task sequence without consecutive duplicate elements, using the supplied async + /// to determine equality of consecutive elements. The comparer returns if two elements are + /// considered equal (and thus the second should be skipped). + /// + /// + /// An async function that returns if two consecutive elements are equal. + /// The input task sequence whose consecutive duplicates will be removed. + /// A sequence without consecutive duplicate elements. + /// + /// Thrown when the input task sequence is null. + static member distinctUntilChangedWithAsync: + comparer: ('T -> 'T -> #Task) -> source: TaskSeq<'T> -> TaskSeq<'T> + + /// + /// Returns a task sequence of each element in the source paired with its successor. + /// The sequence is empty if the source has fewer than two elements. + /// + /// + /// The input task sequence. + /// A task sequence of consecutive element pairs. + /// + /// Thrown when the input task sequence is null. + static member pairwise: source: TaskSeq<'T> -> TaskSeq<'T * 'T> + + /// + /// Divides the input task sequence into chunks of size at most . + /// The last chunk may be smaller than if the source sequence does not divide evenly. + /// Returns an empty task sequence when the source is empty. + /// + /// If is not positive, an is raised immediately + /// (before the sequence is evaluated). + /// + /// + /// The maximum number of elements in each chunk. Must be positive. + /// The input task sequence. + /// A task sequence of non-overlapping array chunks. + /// Thrown when the input task sequence is null. + /// Thrown when is not positive. + static member chunkBySize: chunkSize: int -> source: TaskSeq<'T> -> TaskSeq<'T[]> + + /// + /// Groups consecutive elements of the task sequence by a key derived from each element using + /// , yielding (key, elements[]) pairs. A new group is started + /// each time the key changes from one element to the next. Unlike , + /// only consecutive elements with the same key are merged. + /// If the function is asynchronous, consider using . + /// + /// + /// A function that computes the key for each element. + /// The input task sequence. + /// A task sequence of (key, elements[]) pairs for each run of equal keys. + /// Thrown when the input task sequence is null. + static member chunkBy: projection: ('T -> 'Key) -> source: TaskSeq<'T> -> TaskSeq<'Key * 'T[]> when 'Key: equality + + /// + /// Groups consecutive elements of the task sequence by a key derived from each element using the + /// asynchronous function , yielding (key, elements[]) pairs. + /// A new group is started each time the key changes from one element to the next. + /// Unlike , only consecutive elements with the same key are merged. + /// If the function is synchronous, consider using . + /// + /// + /// An asynchronous function that computes the key for each element. + /// The input task sequence. + /// A task sequence of (key, elements[]) pairs for each run of equal keys. + /// Thrown when the input task sequence is null. + static member chunkByAsync: + projection: ('T -> #Task<'Key>) -> source: TaskSeq<'T> -> TaskSeq<'Key * 'T[]> when 'Key: equality + + /// + /// Returns a task sequence of sliding windows of a given size over the source sequence. + /// Each window is a fresh array of exactly consecutive elements. + /// The result is empty if the source has fewer than elements. + /// + /// Uses a ring buffer internally to avoid redundant copies, yielding one allocation per window. + /// + /// If is not positive, an is raised immediately + /// (before the sequence is evaluated). + /// + /// + /// The number of elements in each window. Must be positive. + /// The input task sequence. + /// A task sequence of overlapping array windows. + /// Thrown when the input task sequence is null. + /// Thrown when is not positive. + static member windowed: windowSize: int -> source: TaskSeq<'T> -> TaskSeq<'T[]> + /// /// Combines the two task sequences into a new task sequence of pairs. The two sequences need not have equal lengths: /// when one sequence is exhausted any remaining elements in the other sequence are ignored. @@ -1309,8 +1723,119 @@ type TaskSeq = static member zip: source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> TaskSeq<'T * 'U> /// - /// Applies the function to each element in the task sequence, threading an accumulator - /// argument of type through the computation. If the input function is and the elements are + /// Combines the three task sequences into a new task sequence of triples. The three sequences need not have equal lengths: + /// when one sequence is exhausted any remaining elements in the other sequences are ignored. + /// + /// + /// The first input task sequence. + /// The second input task sequence. + /// The third input task sequence. + /// The result task sequence of triples. + /// Thrown when any of the three input task sequences is null. + static member zip3: + source1: TaskSeq<'T1> -> source2: TaskSeq<'T2> -> source3: TaskSeq<'T3> -> TaskSeq<'T1 * 'T2 * 'T3> + + /// + /// Combines two task sequences and applies the given function to corresponding + /// element pairs. The sequences need not have equal lengths: when one is exhausted any remaining elements in + /// the other are ignored. + /// If the function is asynchronous, consider using . + /// + /// + /// A function to apply to each element pair. + /// The first input task sequence. + /// The second input task sequence. + /// The result task sequence of mapped values. + /// Thrown when either of the two input task sequences is null. + static member zipWith: mapping: ('T -> 'U -> 'V) -> source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> TaskSeq<'V> + + /// + /// Combines two task sequences and applies the given asynchronous function to + /// corresponding element pairs. The sequences need not have equal lengths: when one is exhausted any remaining + /// elements in the other are ignored. + /// If the function is synchronous, consider using . + /// + /// + /// An asynchronous function to apply to each element pair. + /// The first input task sequence. + /// The second input task sequence. + /// The result task sequence of mapped values. + /// Thrown when either of the two input task sequences is null. + static member zipWithAsync: + mapping: ('T -> 'U -> #Task<'V>) -> source1: TaskSeq<'T> -> source2: TaskSeq<'U> -> TaskSeq<'V> + + /// + /// Combines three task sequences and applies the given function to corresponding + /// element triples. The sequences need not have equal lengths: when one is exhausted any remaining elements in + /// the others are ignored. + /// If the function is asynchronous, consider using . + /// + /// + /// A function to apply to each element triple. + /// The first input task sequence. + /// The second input task sequence. + /// The third input task sequence. + /// The result task sequence of mapped values. + /// Thrown when any of the three input task sequences is null. + static member zipWith3: + mapping: ('T1 -> 'T2 -> 'T3 -> 'V) -> + source1: TaskSeq<'T1> -> + source2: TaskSeq<'T2> -> + source3: TaskSeq<'T3> -> + TaskSeq<'V> + + /// + /// Combines three task sequences and applies the given asynchronous function to + /// corresponding element triples. The sequences need not have equal lengths: when one is exhausted any remaining + /// elements in the others are ignored. + /// If the function is synchronous, consider using . + /// + /// + /// An asynchronous function to apply to each element triple. + /// The first input task sequence. + /// The second input task sequence. + /// The third input task sequence. + /// The result task sequence of mapped values. + /// Thrown when any of the three input task sequences is null. + static member zipWithAsync3: + mapping: ('T1 -> 'T2 -> 'T3 -> #Task<'V>) -> + source1: TaskSeq<'T1> -> + source2: TaskSeq<'T2> -> + source3: TaskSeq<'T3> -> + TaskSeq<'V> + + /// + /// Applies a comparer function to corresponding elements of two task sequences, returning the result of the + /// first comparison that is non-zero, or zero if all compared elements are equal. The sequences are compared + /// element by element until one of them is exhausted; if one sequence is shorter than the other, it is considered + /// less than the longer sequence. + /// If the comparer function is asynchronous, consider using . + /// + /// + /// A function that compares an element from the first sequence with one from the second, returning an integer (negative = less than, zero = equal, positive = greater than). + /// The first input task sequence. + /// The second input task sequence. + /// A task returning the first non-zero comparison result, or zero if all elements compare equal and the sequences have equal length. + /// Thrown when either input task sequence is null. + static member compareWith: comparer: ('T -> 'T -> int) -> source1: TaskSeq<'T> -> source2: TaskSeq<'T> -> Task + + /// + /// Applies an asynchronous comparer function to corresponding elements of two task sequences, returning the result of + /// the first comparison that is non-zero, or zero if all compared elements are equal. The sequences are compared + /// element by element until one of them is exhausted; if one sequence is shorter than the other, it is considered + /// less than the longer sequence. + /// If the comparer function is synchronous, consider using . + /// + /// + /// An asynchronous function that compares an element from the first sequence with one from the second, returning an integer (negative = less than, zero = equal, positive = greater than). + /// The first input task sequence. + /// The second input task sequence. + /// A task returning the first non-zero comparison result, or zero if all elements compare equal and the sequences have equal length. + /// Thrown when either input task sequence is null. + static member compareWithAsync: + comparer: ('T -> 'T -> #Task) -> source1: TaskSeq<'T> -> source2: TaskSeq<'T> -> Task + + /// then computes. /// If the accumulator function is asynchronous, consider using . /// argument of type through the computation. If the input function is and the elements are @@ -1340,6 +1865,244 @@ type TaskSeq = static member foldAsync: folder: ('State -> 'T -> #Task<'State>) -> state: 'State -> source: TaskSeq<'T> -> Task<'State> + /// + /// Like , but returns the sequence of intermediate results and the final result. + /// The first element of the output sequence is always the initial state. If the input task sequence + /// has N elements, the output task sequence has N + 1 elements. + /// If the folder function is asynchronous, consider using . + /// + /// + /// A function that updates the state with each element from the sequence. + /// The initial state. + /// The input sequence. + /// A task sequence of states, starting with the initial state and applying the folder to each element. + /// Thrown when the input task sequence is null. + static member scan: folder: ('State -> 'T -> 'State) -> state: 'State -> source: TaskSeq<'T> -> TaskSeq<'State> + + /// + /// Like , but returns the sequence of intermediate results and the final result. + /// The first element of the output sequence is always the initial state. If the input task sequence + /// has N elements, the output task sequence has N + 1 elements. + /// If the folder function is synchronous, consider using . + /// + /// + /// A function that updates the state with each element from the sequence. + /// The initial state. + /// The input sequence. + /// A task sequence of states, starting with the initial state and applying the folder to each element. + /// Thrown when the input task sequence is null. + static member scanAsync: + folder: ('State -> 'T -> #Task<'State>) -> state: 'State -> source: TaskSeq<'T> -> TaskSeq<'State> + + /// + /// Applies the function to each element of the task sequence, threading an accumulator + /// argument through the computation, while also generating a new mapped element for each input element. + /// If the input function is and the elements are , then + /// computes both the mapped results and the final state in a single pass. + /// The result is a pair of an array of mapped values and the final state. + /// If the mapping function is asynchronous, consider using . + /// + /// + /// A function that maps each element to a result while also updating the state. + /// The initial state. + /// The input task sequence. + /// A task returning a pair of the array of mapped results and the final state. + /// Thrown when the input task sequence is null. + static member mapFold: + mapping: ('State -> 'T -> 'Result * 'State) -> state: 'State -> source: TaskSeq<'T> -> Task<'Result[] * 'State> + + /// + /// Applies the asynchronous function to each element of the task sequence, + /// threading an accumulator argument through the computation, while also generating a new mapped element for each input element. + /// If the input function is and the elements are , then + /// computes both the mapped results and the final state in a single pass. + /// The result is a pair of an array of mapped values and the final state. + /// If the mapping function is synchronous, consider using . + /// + /// + /// An asynchronous function that maps each element to a result while also updating the state. + /// The initial state. + /// The input task sequence. + /// A task returning a pair of the array of mapped results and the final state. + /// Thrown when the input task sequence is null. + static member mapFoldAsync: + mapping: ('State -> 'T -> #Task<'Result * 'State>) -> + state: 'State -> + source: TaskSeq<'T> -> + Task<'Result[] * 'State> + + /// + /// Applies the function to each element of the task sequence, threading a state + /// argument through the computation, and lazily yields each mapped result as a new task sequence. Unlike + /// , the results are streamed rather than collected into an array, and the + /// final state is not returned. + /// If the function is asynchronous, consider using . + /// + /// + /// A function that maps each element to a result while also updating the state. + /// The initial state. + /// The input task sequence. + /// A task sequence of mapped results, produced lazily in source order. + /// Thrown when the input task sequence is null. + static member threadState: + folder: ('State -> 'T -> 'Result * 'State) -> state: 'State -> source: TaskSeq<'T> -> TaskSeq<'Result> + + /// + /// Applies the asynchronous function to each element of the task sequence, threading + /// a state argument through the computation, and lazily yields each mapped result as a new task sequence. Unlike + /// , the results are streamed rather than collected into an array, and the + /// final state is not returned. + /// If the function is synchronous, consider using . + /// + /// + /// An asynchronous function that maps each element to a result while also updating the state. + /// The initial state. + /// The input task sequence. + /// A task sequence of mapped results, produced lazily in source order. + /// Thrown when the input task sequence is null. + static member threadStateAsync: + folder: ('State -> 'T -> #Task<'Result * 'State>) -> state: 'State -> source: TaskSeq<'T> -> TaskSeq<'Result> + + + /// + /// Applies the function to each element of the task sequence, threading + /// an accumulator argument through the computation. The first element is used as the initial state. If the input + /// function is and the elements are , then computes + /// . Raises when the + /// sequence is empty. + /// If the accumulator function is asynchronous, consider using . + /// + /// + /// A function that updates the state with each element from the sequence. + /// The input sequence. + /// The final state value after applying the reduction function to all elements. + /// Thrown when the input task sequence is null. + /// Thrown when the input task sequence is empty. + static member reduce: folder: ('T -> 'T -> 'T) -> source: TaskSeq<'T> -> Task<'T> + + /// + /// Applies the asynchronous function to each element of the task sequence, threading + /// an accumulator argument through the computation. The first element is used as the initial state. If the input + /// function is and the elements are , then computes + /// . Raises when the + /// sequence is empty. + /// If the accumulator function is synchronous, consider using . + /// + /// + /// A function that updates the state with each element from the sequence. + /// The input sequence. + /// The final state value after applying the reduction function to all elements. + /// Thrown when the input task sequence is null. + /// Thrown when the input task sequence is empty. + static member reduceAsync: folder: ('T -> 'T -> #Task<'T>) -> source: TaskSeq<'T> -> Task<'T> + + /// + /// Applies a key-generating function to each element of a task sequence and yields a sequence of unique keys + /// and arrays of all elements that have each key, in order of first occurrence of each key. + /// The returned array preserves the original order of elements within each group. + /// + /// + /// + /// This function consumes the entire source task sequence before returning. + /// If the projection function is asynchronous, consider using + /// . + /// + /// + /// A function that transforms each element into a key. + /// The input task sequence. + /// A task returning an array of (key, elements[]) pairs. + /// Thrown when the input task sequence is null. + static member groupBy: projection: ('T -> 'Key) -> source: TaskSeq<'T> -> Task<('Key * 'T[])[]> when 'Key: equality + + /// + /// Applies an asynchronous key-generating function to each element of a task sequence and yields a sequence of + /// unique keys and arrays of all elements that have each key, in order of first occurrence of each key. + /// The returned array preserves the original order of elements within each group. + /// + /// + /// + /// This function consumes the entire source task sequence before returning. + /// If the projection function is synchronous, consider using + /// . + /// + /// + /// An asynchronous function that transforms each element into a key. + /// The input task sequence. + /// A task returning an array of (key, elements[]) pairs. + /// Thrown when the input task sequence is null. + static member groupByAsync: + projection: ('T -> #Task<'Key>) -> source: TaskSeq<'T> -> Task<('Key * 'T[])[]> when 'Key: equality + + /// + /// Applies a key-generating function to each element of a task sequence and returns a task with an array of + /// unique keys and their element counts, in order of first occurrence of each key. + /// + /// + /// + /// This function consumes the entire source task sequence before returning. + /// If the projection function is asynchronous, consider using + /// . + /// + /// + /// A function that transforms each element into a key. + /// The input task sequence. + /// A task returning an array of (key, count) pairs. + /// Thrown when the input task sequence is null. + static member countBy: projection: ('T -> 'Key) -> source: TaskSeq<'T> -> Task<('Key * int)[]> when 'Key: equality + + /// + /// Applies an asynchronous key-generating function to each element of a task sequence and returns a task with + /// an array of unique keys and their element counts, in order of first occurrence of each key. + /// + /// + /// + /// This function consumes the entire source task sequence before returning. + /// If the projection function is synchronous, consider using + /// . + /// + /// + /// An asynchronous function that transforms each element into a key. + /// The input task sequence. + /// A task returning an array of (key, count) pairs. + /// Thrown when the input task sequence is null. + static member countByAsync: + projection: ('T -> #Task<'Key>) -> source: TaskSeq<'T> -> Task<('Key * int)[]> when 'Key: equality + + /// + /// Splits the task sequence into two arrays: those for which the given predicate returns true, + /// and those for which it returns false. The relative order of elements within each partition is preserved. + /// + /// + /// + /// This function consumes the entire source task sequence before returning. + /// If the predicate function is asynchronous, consider using + /// . + /// + /// + /// A function that returns true for elements to include in the first array. + /// The input task sequence. + /// A task returning a tuple of two arrays: (trueItems, falseItems). + /// Thrown when the input task sequence is null. + static member partition: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task<'T[] * 'T[]> + + /// + /// Splits the task sequence into two arrays using an asynchronous predicate: those for which the predicate returns + /// true, and those for which it returns false. The relative order of elements within each partition + /// is preserved. + /// + /// + /// + /// This function consumes the entire source task sequence before returning. + /// If the predicate function is synchronous, consider using + /// . + /// + /// + /// An asynchronous function that returns true for elements to include in the first array. + /// The input task sequence. + /// A task returning a tuple of two arrays: (trueItems, falseItems). + /// Thrown when the input task sequence is null. + static member partitionAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> Task<'T[] * 'T[]> + /// /// Return a new task sequence with a new item inserted before the given index. /// diff --git a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs index 958678ab..9a8fd826 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs @@ -4,6 +4,7 @@ open System.Diagnostics // note: this is *not* an experimental feature, but they forgot to switch off the flag #nowarn "57" // Experimental library feature, requires '--langversion:preview'. +#nowarn "3513" // Resumable code invocation: intentionally calling ResumableCode as a regular delegate in the dynamic (FSI) fallback path. open System open System.Collections.Generic @@ -242,6 +243,11 @@ and [] TaskSeq<'Machine, 'T Debug.logInfo "at MoveNextAsync: normal resumption scenario" let data = this._machine.Data + + // Honor the cancellation token passed to GetAsyncEnumerator (fixes #179). + // ThrowIfCancellationRequested() is a no-op for CancellationToken.None. + data.cancellationToken.ThrowIfCancellationRequested() + data.promiseOfValueOrEnd.Reset() let mutable ts = this @@ -311,6 +317,182 @@ and TaskSeqStateMachine<'T> = ResumableStateMachine> and TaskSeqResumptionFunc<'T> = ResumptionFunc> and TaskSeqResumptionDynamicInfo<'T> = ResumptionDynamicInfo> +/// Implements the dynamic (FSI) path for ResumptionDynamicInfo, used by TaskSeqDynamic. +/// Handles the state-machine transitions that the compiler-generated MoveNextMethodImpl handles in the static path. +and [] TaskSeqDynamicInfo<'T>(initialResumptionFunc: TaskSeqResumptionFunc<'T>) = + inherit TaskSeqResumptionDynamicInfo<'T>(initialResumptionFunc) + + override this.MoveNext(sm: byref>) = + try + Debug.logInfo "at TaskSeqDynamicInfo.MoveNext start" + + let __stack_code_fin = this.ResumptionFunc.Invoke(&sm) + + if __stack_code_fin then + Debug.logInfo "at TaskSeqDynamicInfo.MoveNext, done" + + sm.Data.promiseOfValueOrEnd.SetResult(false) + sm.Data.builder.Complete() + sm.Data.completed <- true + + elif sm.Data.current.IsSome then + Debug.logInfo "at TaskSeqDynamicInfo.MoveNext, still more items" + + sm.Data.promiseOfValueOrEnd.SetResult(true) + + else + Debug.logInfo "at TaskSeqDynamicInfo.MoveNext, await" + + let boxed = sm.Data.boxedSelf + + sm.Data.awaiter.UnsafeOnCompleted(fun () -> + let mutable boxed = boxed + moveNextRef &boxed) + + with exn -> + Debug.logInfo ("Setting exception of PromiseOfValueOrEnd to: ", exn.Message) + sm.Data.promiseOfValueOrEnd.SetException(exn) + sm.Data.builder.Complete() + + override _.SetStateMachine(_machine: byref>, _state: IAsyncStateMachine) = () + +/// Dynamic (FSI) implementation of IAsyncEnumerable for taskSeq computation expressions. +/// Used when the F# compiler cannot emit static resumable code (e.g., in F# Interactive). +and [] TaskSeqDynamic<'T>() = + inherit TaskSeqBase<'T>() + + let initialThreadId = Environment.CurrentManagedThreadId + + [] + val mutable _machine: TaskSeqStateMachine<'T> + + [] + val mutable _initialResumptionFunc: TaskSeqResumptionFunc<'T> + + member this.InitDynamicMachineData(ct: CancellationToken) = + let data = TaskSeqStateMachineData() + data.boxedSelf <- this + data.cancellationToken <- ct + data.builder <- AsyncIteratorMethodBuilder.Create() + this._machine.Data <- data + this._machine.ResumptionDynamicInfo <- TaskSeqDynamicInfo(this._initialResumptionFunc) + + interface IValueTaskSource with + member this.GetResult token = + let canMoveNext = this._machine.Data.promiseOfValueOrEnd.GetResult token + + if not canMoveNext then + this._machine.Data.completed <- true + + member this.GetStatus token = this._machine.Data.promiseOfValueOrEnd.GetStatus token + + member this.OnCompleted(continuation, state, token, flags) = + this._machine.Data.promiseOfValueOrEnd.OnCompleted(continuation, state, token, flags) + + interface IValueTaskSource with + member this.GetStatus token = this._machine.Data.promiseOfValueOrEnd.GetStatus token + + member this.GetResult token = + let canMoveNext = this._machine.Data.promiseOfValueOrEnd.GetResult token + + if not canMoveNext then + this._machine.Data.completed <- true + + canMoveNext + + member this.OnCompleted(continuation, state, token, flags) = + this._machine.Data.promiseOfValueOrEnd.OnCompleted(continuation, state, token, flags) + + interface IAsyncStateMachine with + member this.MoveNext() = moveNextRef &this._machine + member _.SetStateMachine(_state) = () + + interface IAsyncEnumerable<'T> with + member this.GetAsyncEnumerator(ct) = + match this._machine.Data :> obj with + | null when initialThreadId = Environment.CurrentManagedThreadId -> + this.InitDynamicMachineData(ct) + this + | _ -> + Debug.logInfo "TaskSeqDynamic.GetAsyncEnumerator, cloning..." + let clone = TaskSeqDynamic<'T>() + clone._initialResumptionFunc <- this._initialResumptionFunc + clone.InitDynamicMachineData(ct) + clone + + interface IAsyncEnumerator<'T> with + member this.Current = + match this._machine.Data.current with + | ValueSome x -> x + | ValueNone -> Unchecked.defaultof<'T> + + member this.MoveNextAsync() = + Debug.logInfo "TaskSeqDynamic.MoveNextAsync..." + + if this._machine.ResumptionPoint = -1 then + Debug.logInfo "at TaskSeqDynamic.MoveNextAsync: Resumption point = -1" + ValueTask.False + + elif this._machine.Data.completed then + Debug.logInfo "at TaskSeqDynamic.MoveNextAsync: completed = true" + this._machine.Data.promiseOfValueOrEnd.Reset() + ValueTask.False + + else + Debug.logInfo "at TaskSeqDynamic.MoveNextAsync: normal resumption" + let data = this._machine.Data + data.cancellationToken.ThrowIfCancellationRequested() + data.promiseOfValueOrEnd.Reset() + let mutable ts = this + data.builder.MoveNext(&ts) + this.MoveNextAsyncResult() + + member this.DisposeAsync() = + task { + match this._machine.Data.disposalStack with + | null -> () + | _ -> + let mutable exn = None + + for d in Seq.rev this._machine.Data.disposalStack do + try + do! d () + with e -> + if exn.IsNone then + exn <- Some e + + match exn with + | None -> () + | Some e -> raise e + } + |> ValueTask + + override this.MoveNextAsyncResult() = + let data = this._machine.Data + let version = data.promiseOfValueOrEnd.Version + let status = data.promiseOfValueOrEnd.GetStatus(version) + + match status with + | ValueTaskSourceStatus.Succeeded -> + Debug.logInfo "at TaskSeqDynamic MoveNextAsyncResult: case succeeded..." + + let result = data.promiseOfValueOrEnd.GetResult(version) + + if not result then + data.current <- ValueNone + + ValueTask.fromResult result + + | ValueTaskSourceStatus.Faulted + | ValueTaskSourceStatus.Canceled + | ValueTaskSourceStatus.Pending as state -> + Debug.logInfo ("at TaskSeqDynamic MoveNextAsyncResult: case ", state) + + ValueTask.ofSource this version + | _ -> + Debug.logInfo "at TaskSeqDynamic MoveNextAsyncResult: Unexpected state" + ValueTask.ofSource this version + type TaskSeqBuilder() = member inline _.Delay(f: unit -> ResumableTSC<'T>) = ResumableTSC<'T>(fun sm -> f().Invoke(&sm)) @@ -374,16 +556,11 @@ type TaskSeqBuilder() = ts._machine <- sm ts :> IAsyncEnumerable<'T>)) else - // let initialResumptionFunc = TaskSeqResumptionFunc<'T>(fun sm -> code.Invoke(&sm)) - // let resumptionFuncExecutor = TaskSeqResumptionExecutor<'T>(fun sm f -> - // // TODO: add exception handling? - // if f.Invoke(&sm) then - // sm.ResumptionPoint <- -2) - // let setStateMachine = SetStateMachineMethodImpl<_>(fun sm f -> ()) - // sm.Machine.ResumptionFuncInfo <- (initialResumptionFunc, resumptionFuncExecutor, setStateMachine) - //sm.Start() - NotImplementedException "No dynamic implementation for TaskSeq yet." - |> raise + // Dynamic path, used when __useResumableCode = false (e.g., in F# Interactive / FSI). + // Uses TaskSeqDynamic which drives the resumable code via ResumptionDynamicInfo. + let ts = TaskSeqDynamic<'T>() + ts._initialResumptionFunc <- TaskSeqResumptionFunc<'T>(fun sm -> code.Invoke(&sm)) + ts :> IAsyncEnumerable<'T> member inline _.Zero() : ResumableTSC<'T> = @@ -525,24 +702,22 @@ module LowPriority = // and we need a way to distinguish these two methods. // // Types handled: - // - ValueTask (non-generic, because it implements GetResult() -> unit) + // - (non-generic) ValueTask (because it implements GetResult() -> unit) // - ValueTask<'T> (because it implements GetResult() -> 'TResult) - // - Task (non-generic, because it implements GetResult() -> unit) + // - (non-generic) Task (because it implements GetResult() -> unit) // - any other type that implements GetAwaiter() // // Not handled: // - Task<'T> (because it only implements GetResult() -> unit, not GetResult() -> 'TResult) [] - member inline _.Bind< ^TaskLike, 'T, 'U, ^Awaiter, 'TOverall + member inline _.Bind< ^TaskLike, 'T, 'U, ^Awaiter when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter) and ^Awaiter :> ICriticalNotifyCompletion and ^Awaiter: (member get_IsCompleted: unit -> bool) and ^Awaiter: (member GetResult: unit -> 'T)> - ( - task: ^TaskLike, - continuation: ('T -> ResumableTSC<'U>) - ) = + (task: ^TaskLike, continuation: ('T -> ResumableTSC<'U>)) + = ResumableTSC<'U>(fun sm -> let mutable awaiter = (^TaskLike: (member GetAwaiter: unit -> ^Awaiter) (task)) @@ -651,10 +826,7 @@ module HighPriority = member inline _.Bind(computation: Async<'T>, continuation: ('T -> ResumableTSC<'U>)) = ResumableTSC<'U>(fun sm -> - let mutable awaiter = - Async - .StartImmediateAsTask(computation, cancellationToken = sm.Data.cancellationToken) - .GetAwaiter() + let mutable awaiter = Async.StartImmediateAsTask(computation, cancellationToken = sm.Data.cancellationToken).GetAwaiter() let mutable __stack_fin = true @@ -685,3 +857,15 @@ module HighPriority = module TaskSeqBuilder = /// Builds an asynchronous task sequence based on IAsyncEnumerable<'T> using computation expression syntax. let taskSeq = TaskSeqBuilder() + +/// Builder for computation expressions. Inherits all members from +/// , using the dynamic (ResumptionDynamicInfo-based) path when the +/// F# compiler cannot emit static resumable code (e.g., in F# Interactive). +type TaskSeqDynamicBuilder() = + inherit TaskSeqBuilder() + +[] +module TaskSeqDynamicBuilder = + /// Builds an asynchronous task sequence, with a dynamic resumable code fallback for scenarios + /// where the F# compiler cannot generate static resumable code (e.g., in F# Interactive / FSI). + let taskSeqDynamic = TaskSeqDynamicBuilder() diff --git a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi index 4c185be6..9caa7091 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi @@ -48,6 +48,8 @@ and ResumableTSC<'T> = ResumableCode, unit> /// For use in this library only. Required by the method. /// and TaskSeqStateMachine<'T> = ResumableStateMachine> +and TaskSeqResumptionFunc<'T> = ResumptionFunc> +and TaskSeqResumptionDynamicInfo<'T> = ResumptionDynamicInfo> /// /// Contains the state data for the computation expression builder. @@ -133,6 +135,40 @@ and [] TaskSeq<'Machine, 'T member InitMachineData: ct: CancellationToken * machine: byref<'Machine> -> unit override MoveNextAsyncResult: unit -> ValueTask +/// +/// Concrete implementation of for taskSeq computation +/// expressions, used in the dynamic (FSI) path. Handles state-machine transitions when the F# compiler +/// cannot generate static resumable code. +/// For use by this library only. +/// +and [] TaskSeqDynamicInfo<'T> = + inherit TaskSeqResumptionDynamicInfo<'T> + new: initialResumptionFunc: TaskSeqResumptionFunc<'T> -> TaskSeqDynamicInfo<'T> + +/// +/// Dynamic (FSI-compatible) implementation of for taskSeq +/// computation expressions. Used when the F# compiler cannot generate static resumable code (e.g., in FSI). +/// For use by this library only. +/// +and [] TaskSeqDynamic<'T> = + inherit TaskSeqBase<'T> + interface IAsyncEnumerator<'T> + interface IAsyncEnumerable<'T> + interface IAsyncStateMachine + interface IValueTaskSource + interface IValueTaskSource + + new: unit -> TaskSeqDynamic<'T> + + [] + val mutable _machine: TaskSeqStateMachine<'T> + + [] + val mutable _initialResumptionFunc: TaskSeqResumptionFunc<'T> + + member InitDynamicMachineData: ct: CancellationToken -> unit + override MoveNextAsyncResult: unit -> ValueTask + /// /// Main builder class for the computation expression. /// @@ -173,7 +209,7 @@ module LowPriority = type TaskSeqBuilder with [] - member inline Bind< ^TaskLike, 'T, 'U, ^Awaiter, 'TOverall> : + member inline Bind< ^TaskLike, 'T, 'U, ^Awaiter> : task: ^TaskLike * continuation: ('T -> ResumableTSC<'U>) -> ResumableTSC<'U> when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter) and ^Awaiter :> ICriticalNotifyCompletion @@ -209,3 +245,22 @@ module HighPriority = member inline Bind: task: Task<'T> * continuation: ('T -> ResumableTSC<'U>) -> ResumableTSC<'U> member inline Bind: computation: Async<'T> * continuation: ('T -> ResumableTSC<'U>) -> ResumableTSC<'U> + +/// +/// Builder class for the computation expression. Inherits all members +/// from , using the dynamic resumable code path as fallback when the +/// F# compiler cannot generate static resumable code (e.g., in F# Interactive / FSI). +/// +[] +type TaskSeqDynamicBuilder = + inherit TaskSeqBuilder + new: unit -> TaskSeqDynamicBuilder + +[] +module TaskSeqDynamicBuilder = + + /// + /// Builds an asynchronous task sequence, with a dynamic resumable code fallback for scenarios + /// where the F# compiler cannot generate static resumable code (e.g., in F# Interactive / FSI). + /// + val taskSeqDynamic: TaskSeqDynamicBuilder diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index d7773ceb..7e73fb4e 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -39,6 +39,11 @@ type internal ChooserAction<'T, 'U, 'TaskOption when 'TaskOption :> Task<'U opti | TryPick of try_pick: ('T -> 'U option) | TryPickAsync of async_try_pick: ('T -> 'TaskOption) +[] +type internal ChooserVAction<'T, 'U, 'TaskValueOption when 'TaskValueOption :> Task<'U voption>> = + | TryPickV of try_pickv: ('T -> 'U voption) + | TryPickVAsync of async_try_pickv: ('T -> 'TaskValueOption) + [] type internal PredicateAction<'T, 'TaskBool when 'TaskBool :> Task> = | Predicate of try_filter: ('T -> bool) @@ -49,6 +54,16 @@ type internal InitAction<'T, 'TaskT when 'TaskT :> Task<'T>> = | InitAction of init_item: (int -> 'T) | InitActionAsync of async_init_item: (int -> 'TaskT) +[] +type internal ProjectorAction<'T, 'Key, 'TaskKey when 'TaskKey :> Task<'Key>> = + | ProjectorAction of projector: ('T -> 'Key) + | AsyncProjectorAction of async_projector: ('T -> 'TaskKey) + +[] +type internal MapFolderAction<'T, 'State, 'Result, 'TaskResultState when 'TaskResultState :> Task<'Result * 'State>> = + | MapFolderAction of map_folder_action: ('State -> 'T -> 'Result * 'State) + | AsyncMapFolderAction of async_map_folder_action: ('State -> 'T -> 'TaskResultState) + [] type internal ManyOrOne<'T> = | Many of source_seq: TaskSeq<'T> @@ -62,7 +77,21 @@ module internal TaskSeqInternal = let inline raiseEmptySeq () = invalidArg "source" "The input task sequence was empty." - let inline raiseCannotBeNegative name = invalidArg name "The value must be non-negative." + /// Moves the enumerator to its first element, assuming it has just been allocated. + /// Raises "The input sequence was empty" if there was no first element. + let inline moveFirstOrRaiseUnsafe (e: IAsyncEnumerator<_>) = task { + let! hasFirst = e.MoveNextAsync() + + if not hasFirst then + invalidArg "source" "The input task sequence was empty." + } + + /// Tests the given integer value and raises if it is -1 or lower. + let inline raiseCannotBeNegative name value = + if value >= 0 then + () + else + invalidArg name $"The value must be non-negative, but was {value}." let inline raiseOutOfBounds name = invalidArg name "The value or index must be within the bounds of the task sequence." @@ -120,42 +149,60 @@ module internal TaskSeqInternal = } } + let replicate count value = + raiseCannotBeNegative (nameof count) count + + taskSeq { + for _ in 1..count do + yield value + } + + let replicateInfinite value = taskSeq { + while true do + yield value + } + + let replicateInfiniteAsync (computation: unit -> #Task<'T>) = taskSeq { + while true do + let! value = computation () + yield value + } + + let replicateUntilNoneAsync (computation: unit -> #Task<'T option>) = taskSeq { + let mutable go = true + + while go do + let! result = computation () + + match result with + | Some value -> yield value + | None -> go <- false + } + /// Returns length unconditionally, or based on a predicate let lengthBy predicate (source: TaskSeq<_>) = checkNonNull (nameof source) source task { - use e = source.GetAsyncEnumerator CancellationToken.None - let mutable go = true let mutable i = 0 - let! step = e.MoveNextAsync() - go <- step match predicate with | None -> - while go do - let! step = e.MoveNextAsync() - i <- i + 1 // update before moving: we are counting, not indexing - go <- step + while! e.MoveNextAsync() do + i <- i + 1 | Some(Predicate predicate) -> - while go do + while! e.MoveNextAsync() do if predicate e.Current then i <- i + 1 - let! step = e.MoveNextAsync() - go <- step - | Some(PredicateAsync predicate) -> - while go do + while! e.MoveNextAsync() do match! predicate e.Current with | true -> i <- i + 1 | false -> () - let! step = e.MoveNextAsync() - go <- step - return i } @@ -165,15 +212,13 @@ module internal TaskSeqInternal = task { use e = source.GetAsyncEnumerator CancellationToken.None - let mutable go = true let mutable i = 0 - let! step = e.MoveNextAsync() - go <- step + let mutable go = true while go && i < max do - i <- i + 1 // update before moving: we are counting, not indexing - let! step = e.MoveNextAsync() - go <- step + let! hasMore = e.MoveNextAsync() + + if hasMore then i <- i + 1 else go <- false return i } @@ -183,10 +228,7 @@ module internal TaskSeqInternal = task { use e = source.GetAsyncEnumerator CancellationToken.None - let! nonEmpty = e.MoveNextAsync() - - if not nonEmpty then - raiseEmptySeq () + do! moveFirstOrRaiseUnsafe e let mutable acc = e.Current @@ -202,10 +244,7 @@ module internal TaskSeqInternal = task { use e = source.GetAsyncEnumerator CancellationToken.None - let! nonEmpty = e.MoveNextAsync() - - if not nonEmpty then - raiseEmptySeq () + do! moveFirstOrRaiseUnsafe e let value = e.Current let mutable accProjection = projection value @@ -228,10 +267,7 @@ module internal TaskSeqInternal = task { use e = source.GetAsyncEnumerator CancellationToken.None - let! nonEmpty = e.MoveNextAsync() - - if not nonEmpty then - raiseEmptySeq () + do! moveFirstOrRaiseUnsafe e let value = e.Current let! projValue = projectionAsync value @@ -272,84 +308,85 @@ module internal TaskSeqInternal = let init count initializer = taskSeq { let mutable i = 0 - let mutable value: Lazy<'T> = Unchecked.defaultof<_> let count = match count with - | Some c -> if c >= 0 then c else raiseCannotBeNegative (nameof count) + | Some c -> + raiseCannotBeNegative (nameof count) c + c + | None -> Int32.MaxValue match initializer with | InitAction init -> while i < count do - // using Lazy gives us locking and safe multiple access to the cached value, if - // multiple threads access the same item through the same enumerator (which is - // bad practice, but hey, who're we to judge). - if isNull value then - value <- Lazy<_>.Create(fun () -> init i) - - yield value.Force() - value <- Unchecked.defaultof<_> + yield init i i <- i + 1 | InitActionAsync asyncInit -> while i < count do - // using Lazy gives us locking and safe multiple access to the cached value, if - // multiple threads access the same item through the same enumerator (which is - // bad practice, but hey, who're we to judge). - if isNull value then - // TODO: is there a 'Lazy' we can use with Task? - let! value' = asyncInit i - value <- Lazy<_>.CreateFromValue value' - - yield value.Force() - value <- Unchecked.defaultof<_> + let! result = asyncInit i + yield result i <- i + 1 } + let unfold generator state = taskSeq { + let mutable go = true + let mutable currentState = state + + while go do + match generator currentState with + | None -> go <- false + | Some(value, nextState) -> + yield value + currentState <- nextState + } + + let unfoldAsync generator state = taskSeq { + let mutable go = true + let mutable currentState = state + + while go do + let! result = (generator currentState: Task<_>) + + match result with + | None -> go <- false + | Some(value, nextState) -> + yield value + currentState <- nextState + } + let iter action (source: TaskSeq<_>) = checkNonNull (nameof source) source task { use e = source.GetAsyncEnumerator CancellationToken.None - let mutable go = true - let! step = e.MoveNextAsync() - go <- step - // this ensures that the inner loop is optimized for the closure - // though perhaps we need to split into individual functions after all to use - // InlineIfLambda? + // Each branch keeps its own while! loop so the match dispatch is hoisted out and + // the JIT sees a tight, single-case loop (same pattern as sum/sumBy etc.). match action with | CountableAction action -> let mutable i = 0 - while go do - do action i e.Current - let! step = e.MoveNextAsync() + while! e.MoveNextAsync() do + action i e.Current i <- i + 1 - go <- step | SimpleAction action -> - while go do - do action e.Current - let! step = e.MoveNextAsync() - go <- step + while! e.MoveNextAsync() do + action e.Current | AsyncCountableAction action -> let mutable i = 0 - while go do + while! e.MoveNextAsync() do do! action i e.Current - let! step = e.MoveNextAsync() i <- i + 1 - go <- step | AsyncSimpleAction action -> - while go do + while! e.MoveNextAsync() do do! action e.Current - let! step = e.MoveNextAsync() - go <- step } let fold folder initial (source: TaskSeq<_>) = @@ -357,34 +394,127 @@ module internal TaskSeqInternal = task { use e = source.GetAsyncEnumerator CancellationToken.None - let mutable go = true let mutable result = initial - let! step = e.MoveNextAsync() - go <- step match folder with | FolderAction folder -> - while go do + while! e.MoveNextAsync() do result <- folder result e.Current - let! step = e.MoveNextAsync() - go <- step | AsyncFolderAction folder -> - while go do + while! e.MoveNextAsync() do let! tempResult = folder result e.Current result <- tempResult - let! step = e.MoveNextAsync() - go <- step return result } - let toResizeArrayAsync source = + let scan folder initial (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + match folder with + | FolderAction folder -> taskSeq { + let mutable state = initial + yield state + + for item in source do + state <- folder state item + yield state + } + + | AsyncFolderAction folder -> taskSeq { + let mutable state = initial + yield state + + for item in source do + let! newState = folder state item + state <- newState + yield state + } + + let reduce folder (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + task { + use e = source.GetAsyncEnumerator CancellationToken.None + let! hasFirst = e.MoveNextAsync() + + if not hasFirst then + raiseEmptySeq () + + let mutable result = e.Current + + match folder with + | FolderAction folder -> + while! e.MoveNextAsync() do + result <- folder result e.Current + + | AsyncFolderAction folder -> + while! e.MoveNextAsync() do + let! tempResult = folder result e.Current + result <- tempResult + + return result + } + + let mapFold (folder: MapFolderAction<_, _, _, _>) initial (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + task { + use e = source.GetAsyncEnumerator CancellationToken.None + let mutable state = initial + let results = ResizeArray() + + match folder with + | MapFolderAction folder -> + while! e.MoveNextAsync() do + let result, newState = folder state e.Current + results.Add result + state <- newState + + | AsyncMapFolderAction folder -> + while! e.MoveNextAsync() do + let! (result, newState) = folder state e.Current + results.Add result + state <- newState + + return results.ToArray(), state + } + + let threadState (folder: 'State -> 'T -> 'U * 'State) initial (source: TaskSeq<'T>) : TaskSeq<'U> = + checkNonNull (nameof source) source + + taskSeq { + let mutable state = initial + + for item in source do + let result, newState = folder state item + state <- newState + yield result + } + + let threadStateAsync (folder: 'State -> 'T -> #Task<'U * 'State>) initial (source: TaskSeq<'T>) : TaskSeq<'U> = + checkNonNull (nameof source) source + + taskSeq { + let mutable state = initial + + for item in source do + let! (result, newState) = folder state item + state <- newState + yield result + } + + let toResizeArrayAsync (source: TaskSeq<'T>) = checkNonNull (nameof source) source task { - let res = ResizeArray() - do! source |> iter (SimpleAction(fun item -> res.Add item)) + let res = ResizeArray<'T>() + use e = source.GetAsyncEnumerator CancellationToken.None + + while! e.MoveNextAsync() do + res.Add e.Current + return res } @@ -441,6 +571,177 @@ module internal TaskSeqInternal = go <- step1 && step2 } + let zip3 (source1: TaskSeq<_>) (source2: TaskSeq<_>) (source3: TaskSeq<_>) = + checkNonNull (nameof source1) source1 + checkNonNull (nameof source2) source2 + checkNonNull (nameof source3) source3 + + taskSeq { + use e1 = source1.GetAsyncEnumerator CancellationToken.None + use e2 = source2.GetAsyncEnumerator CancellationToken.None + use e3 = source3.GetAsyncEnumerator CancellationToken.None + let mutable go = true + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let! step3 = e3.MoveNextAsync() + go <- step1 && step2 && step3 + + while go do + yield e1.Current, e2.Current, e3.Current + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let! step3 = e3.MoveNextAsync() + go <- step1 && step2 && step3 + } + + let zipWith (mapping: 'T -> 'U -> 'V) (source1: TaskSeq<'T>) (source2: TaskSeq<'U>) = + checkNonNull (nameof source1) source1 + checkNonNull (nameof source2) source2 + + taskSeq { + use e1 = source1.GetAsyncEnumerator CancellationToken.None + use e2 = source2.GetAsyncEnumerator CancellationToken.None + let mutable go = true + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + go <- step1 && step2 + + while go do + yield mapping e1.Current e2.Current + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + go <- step1 && step2 + } + + let zipWithAsync (mapping: 'T -> 'U -> #Task<'V>) (source1: TaskSeq<'T>) (source2: TaskSeq<'U>) = + checkNonNull (nameof source1) source1 + checkNonNull (nameof source2) source2 + + taskSeq { + use e1 = source1.GetAsyncEnumerator CancellationToken.None + use e2 = source2.GetAsyncEnumerator CancellationToken.None + let mutable go = true + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + go <- step1 && step2 + + while go do + let! result = mapping e1.Current e2.Current + yield result + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + go <- step1 && step2 + } + + let zipWith3 (mapping: 'T1 -> 'T2 -> 'T3 -> 'V) (source1: TaskSeq<'T1>) (source2: TaskSeq<'T2>) (source3: TaskSeq<'T3>) = + checkNonNull (nameof source1) source1 + checkNonNull (nameof source2) source2 + checkNonNull (nameof source3) source3 + + taskSeq { + use e1 = source1.GetAsyncEnumerator CancellationToken.None + use e2 = source2.GetAsyncEnumerator CancellationToken.None + use e3 = source3.GetAsyncEnumerator CancellationToken.None + let mutable go = true + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let! step3 = e3.MoveNextAsync() + go <- step1 && step2 && step3 + + while go do + yield mapping e1.Current e2.Current e3.Current + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let! step3 = e3.MoveNextAsync() + go <- step1 && step2 && step3 + } + + let zipWithAsync3 (mapping: 'T1 -> 'T2 -> 'T3 -> #Task<'V>) (source1: TaskSeq<'T1>) (source2: TaskSeq<'T2>) (source3: TaskSeq<'T3>) = + checkNonNull (nameof source1) source1 + checkNonNull (nameof source2) source2 + checkNonNull (nameof source3) source3 + + taskSeq { + use e1 = source1.GetAsyncEnumerator CancellationToken.None + use e2 = source2.GetAsyncEnumerator CancellationToken.None + use e3 = source3.GetAsyncEnumerator CancellationToken.None + let mutable go = true + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let! step3 = e3.MoveNextAsync() + go <- step1 && step2 && step3 + + while go do + let! result = mapping e1.Current e2.Current e3.Current + yield result + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let! step3 = e3.MoveNextAsync() + go <- step1 && step2 && step3 + } + + let compareWith (comparer: 'T -> 'T -> int) (source1: TaskSeq<'T>) (source2: TaskSeq<'T>) = + checkNonNull (nameof source1) source1 + checkNonNull (nameof source2) source2 + + task { + use e1 = source1.GetAsyncEnumerator CancellationToken.None + use e2 = source2.GetAsyncEnumerator CancellationToken.None + let mutable result = 0 + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let mutable has1 = step1 + let mutable has2 = step2 + + while result = 0 && (has1 || has2) do + match has1, has2 with + | false, _ -> result <- -1 // source1 is shorter: less than + | _, false -> result <- 1 // source2 is shorter: greater than + | true, true -> + let cmp = comparer e1.Current e2.Current + + if cmp <> 0 then + result <- cmp + else + let! s1 = e1.MoveNextAsync() + let! s2 = e2.MoveNextAsync() + has1 <- s1 + has2 <- s2 + + return result + } + + let compareWithAsync (comparer: 'T -> 'T -> #Task) (source1: TaskSeq<'T>) (source2: TaskSeq<'T>) = + checkNonNull (nameof source1) source1 + checkNonNull (nameof source2) source2 + + task { + use e1 = source1.GetAsyncEnumerator CancellationToken.None + use e2 = source2.GetAsyncEnumerator CancellationToken.None + let mutable result = 0 + let! step1 = e1.MoveNextAsync() + let! step2 = e2.MoveNextAsync() + let mutable has1 = step1 + let mutable has2 = step2 + + while result = 0 && (has1 || has2) do + match has1, has2 with + | false, _ -> result <- -1 // source1 is shorter: less than + | _, false -> result <- 1 // source2 is shorter: greater than + | true, true -> + let! cmp = comparer e1.Current e2.Current + + if cmp <> 0 then + result <- cmp + else + let! s1 = e1.MoveNextAsync() + let! s2 = e2.MoveNextAsync() + has1 <- s1 + has2 <- s2 + + return result + } + let collect (binder: _ -> #IAsyncEnumerable<_>) (source: TaskSeq<_>) = checkNonNull (nameof source) source @@ -480,15 +781,10 @@ module internal TaskSeqInternal = task { use e = source.GetAsyncEnumerator CancellationToken.None - let mutable go = true let mutable last = ValueNone - let! step = e.MoveNextAsync() - go <- step - while go do + while! e.MoveNextAsync() do last <- ValueSome e.Current - let! step = e.MoveNextAsync() - go <- step match last with | ValueSome value -> return Some value @@ -517,18 +813,52 @@ module internal TaskSeqInternal = | true -> return taskSeq { - let mutable go = true - let! step = e.MoveNextAsync() - go <- step - - while go do + while! e.MoveNextAsync() do yield e.Current - let! step = e.MoveNextAsync() - go <- step } |> Some } + let firstOrDefault defaultValue source = + tryHead source + |> Task.map (Option.defaultValue defaultValue) + + let lastOrDefault defaultValue source = + tryLast source + |> Task.map (Option.defaultValue defaultValue) + + let splitAt count (source: TaskSeq<'T>) = + checkNonNull (nameof source) source + + if count < 0 then + invalidArg (nameof count) $"The value must be non-negative, but was {count}." + + task { + use e = source.GetAsyncEnumerator CancellationToken.None + let first = ResizeArray<'T>(count) + let mutable i = 0 + let mutable go = true + + while go && i < count do + let! step = e.MoveNextAsync() + + if step then + first.Add e.Current + i <- i + 1 + else + go <- false + + // 'rest' captures 'e' from the outer task block; if the source was not exhausted, + // advance once past the last element added to 'first', then yield the remainder. + let rest = taskSeq { + if go then + while! e.MoveNextAsync() do + yield e.Current + } + + return first.ToArray(), rest + } + let tryItem index (source: TaskSeq<_>) = checkNonNull (nameof source) source @@ -545,14 +875,14 @@ module internal TaskSeqInternal = let! step = e.MoveNextAsync() go <- step - while go && idx <= index do - if idx = index then - foundItem <- Some e.Current - go <- false - else - let! step = e.MoveNextAsync() - go <- step - idx <- idx + 1 + // advance past the first `index` elements, then capture the current element + while go && idx < index do + let! step = e.MoveNextAsync() + go <- step + idx <- idx + 1 + + if go then + foundItem <- Some e.Current return foundItem } @@ -685,6 +1015,25 @@ module internal TaskSeqInternal = | None -> () } + let chooseV chooser (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + taskSeq { + + match chooser with + | TryPickV picker -> + for item in source do + match picker item with + | ValueSome value -> yield value + | ValueNone -> () + + | TryPickVAsync picker -> + for item in source do + match! picker item with + | ValueSome value -> yield value + | ValueNone -> () + } + let filter predicate (source: TaskSeq<_>) = checkNonNull (nameof source) source @@ -739,11 +1088,103 @@ module internal TaskSeqInternal = return state } - let skipOrTake skipOrTake count (source: TaskSeq<_>) = + /// Direct bool-returning exists, avoiding the Option<'T> allocation that tryFind+isSome would incur. + let exists predicate (source: TaskSeq<_>) = checkNonNull (nameof source) source - if count < 0 then - raiseCannotBeNegative (nameof count) + match predicate with + | Predicate syncPredicate -> task { + use e = source.GetAsyncEnumerator CancellationToken.None + let mutable found = false + let! cont = e.MoveNextAsync() + let mutable hasMore = cont + + while not found && hasMore do + found <- syncPredicate e.Current + + if not found then + let! cont = e.MoveNextAsync() + hasMore <- cont + + return found + } + + | PredicateAsync asyncPredicate -> task { + use e = source.GetAsyncEnumerator CancellationToken.None + let mutable found = false + let! cont = e.MoveNextAsync() + let mutable hasMore = cont + + while not found && hasMore do + let! pred = asyncPredicate e.Current + found <- pred + + if not found then + let! cont = e.MoveNextAsync() + hasMore <- cont + + return found + } + + /// Direct bool-returning contains, avoiding the Option<'T> allocation and closure that tryFind+isSome would incur. + let contains (value: 'T) (source: TaskSeq<'T>) = + checkNonNull (nameof source) source + + task { + use e = source.GetAsyncEnumerator CancellationToken.None + let mutable found = false + let! cont = e.MoveNextAsync() + let mutable hasMore = cont + + while not found && hasMore do + if e.Current = value then + found <- true + else + let! cont = e.MoveNextAsync() + hasMore <- cont + + return found + } + + let distinct (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + taskSeq { + // only create hashset when we start iterating; sequential so plain HashSet suffices + let seen = HashSet<_>(HashIdentity.Structural) + + for item in source do + if seen.Add item then + yield item + } + + let distinctBy (projection: _ -> _) (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + taskSeq { + let seen = HashSet<_>(HashIdentity.Structural) + + for item in source do + if seen.Add(projection item) then + yield item + } + + let distinctByAsync (projection: _ -> #Task<_>) (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + taskSeq { + let seen = HashSet<_>(HashIdentity.Structural) + + for item in source do + let! key = projection item + + if seen.Add key then + yield item + } + + let skipOrTake skipOrTake count (source: TaskSeq<_>) = + checkNonNull (nameof source) source + raiseCannotBeNegative (nameof count) count match skipOrTake with | Skip -> @@ -771,24 +1212,19 @@ module internal TaskSeqInternal = else taskSeq { use e = source.GetAsyncEnumerator CancellationToken.None + let mutable i = 0 + let mutable cont = true - let! step = e.MoveNextAsync() - let mutable cont = step - let mutable pos = 0 - - // skip, or stop looping if we reached the end - while cont do - pos <- pos + 1 - - if pos < count then - let! moveNext = e.MoveNextAsync() - cont <- moveNext - else - cont <- false + // advance past 'count' elements; stop early if the source is shorter + while cont && i < count do + let! hasMore = e.MoveNextAsync() + if hasMore then i <- i + 1 else cont <- false - // return the rest - while! e.MoveNextAsync() do - yield e.Current + // return remaining elements; enumerator is at element (count-1) so one + // more MoveNext is needed to reach element (count) + if cont then + while! e.MoveNextAsync() do + yield e.Current } | Take -> @@ -799,7 +1235,7 @@ module internal TaskSeqInternal = taskSeq { use e = source.GetAsyncEnumerator CancellationToken.None - for _ in count .. - 1 .. 1 do + for _ in count .. -1 .. 1 do let! step = e.MoveNextAsync() if not step then @@ -815,19 +1251,16 @@ module internal TaskSeqInternal = else taskSeq { use e = source.GetAsyncEnumerator CancellationToken.None + let mutable yielded = 0 + let mutable cont = true - let! step = e.MoveNextAsync() - let mutable cont = step - let mutable pos = 0 - - // return items until we've exhausted the seq - while cont do - yield e.Current - pos <- pos + 1 + // yield up to 'count' elements; stop when exhausted or limit reached + while cont && yielded < count do + let! hasMore = e.MoveNextAsync() - if pos < count then - let! moveNext = e.MoveNextAsync() - cont <- moveNext + if hasMore then + yield e.Current + yielded <- yielded + 1 else cont <- false @@ -907,8 +1340,13 @@ module internal TaskSeqInternal = /// InsertAt or InsertManyAt let insertAt index valueOrValues (source: TaskSeq<_>) = - if index < 0 then - raiseCannotBeNegative (nameof index) + checkNonNull (nameof source) source + + match valueOrValues with + | Many values -> checkNonNull "values" values + | One _ -> () + + raiseCannotBeNegative (nameof index) index taskSeq { let mutable i = 0 @@ -933,8 +1371,8 @@ module internal TaskSeqInternal = } let removeAt index (source: TaskSeq<'T>) = - if index < 0 then - raiseCannotBeNegative (nameof index) + checkNonNull (nameof source) source + raiseCannotBeNegative (nameof index) index taskSeq { let mutable i = 0 @@ -951,8 +1389,8 @@ module internal TaskSeqInternal = } let removeManyAt index count (source: TaskSeq<'T>) = - if index < 0 then - raiseCannotBeNegative (nameof index) + checkNonNull (nameof source) source + raiseCannotBeNegative (nameof index) index taskSeq { let mutable i = 0 @@ -970,8 +1408,8 @@ module internal TaskSeqInternal = } let updateAt index value (source: TaskSeq<'T>) = - if index < 0 then - raiseCannotBeNegative (nameof index) + checkNonNull (nameof source) source + raiseCannotBeNegative (nameof index) index taskSeq { let mutable i = 0 @@ -989,109 +1427,340 @@ module internal TaskSeqInternal = raiseOutOfBounds (nameof index) } - // Consider turning using an F# version of this instead? - // https://github.com/i3arnon/ConcurrentHashSet - type ConcurrentHashSet<'T when 'T: equality>(ct) = - let _rwLock = new ReaderWriterLockSlim() - let hashSet = HashSet<'T>(Array.empty, HashIdentity.Structural) - - member _.Add item = - _rwLock.EnterWriteLock() - - try - hashSet.Add item - finally - _rwLock.ExitWriteLock() + let except (itemsToExclude: TaskSeq<_>) (source: TaskSeq<_>) = + checkNonNull (nameof source) source + checkNonNull (nameof itemsToExclude) itemsToExclude - member _.AddMany items = - _rwLock.EnterWriteLock() + taskSeq { + use e = source.GetAsyncEnumerator CancellationToken.None + let! hasFirst = e.MoveNextAsync() - try - for item in items do - hashSet.Add item |> ignore + if hasFirst then + // only create hashset by the time we actually start iterating; + // taskSeq enumerates sequentially, so a plain HashSet suffices — no locking needed. + let hashSet = HashSet<_>(HashIdentity.Structural) - finally - _rwLock.ExitWriteLock() + use excl = itemsToExclude.GetAsyncEnumerator CancellationToken.None - member _.AddManyAsync(source: TaskSeq<'T>) = task { - use e = source.GetAsyncEnumerator(ct) - let mutable go = true - let! step = e.MoveNextAsync() - go <- step + while! excl.MoveNextAsync() do + hashSet.Add excl.Current |> ignore - while go do - // NOTE: r/w lock cannot cross thread boundaries. Should we use SemaphoreSlim instead? - // or alternatively, something like this: https://github.com/StephenCleary/AsyncEx/blob/8a73d0467d40ca41f9f9cf827c7a35702243abb8/src/Nito.AsyncEx.Coordination/AsyncReaderWriterLock.cs#L16 - // not sure how they compare. + // if true, it was added, and therefore unique, so we return it + // if false, it existed, and therefore a duplicate, and we skip + if hashSet.Add e.Current then + yield e.Current - _rwLock.EnterWriteLock() + while! e.MoveNextAsync() do + let current = e.Current - try - hashSet.Add e.Current |> ignore - finally - _rwLock.ExitWriteLock() + if hashSet.Add current then + yield current - let! step = e.MoveNextAsync() - go <- step } - interface IDisposable with - override _.Dispose() = - if not (isNull _rwLock) then - _rwLock.Dispose() - - let except itemsToExclude (source: TaskSeq<_>) = + let exceptOfSeq itemsToExclude (source: TaskSeq<_>) = checkNonNull (nameof source) source checkNonNull (nameof itemsToExclude) itemsToExclude taskSeq { use e = source.GetAsyncEnumerator CancellationToken.None - let mutable go = true - let! step = e.MoveNextAsync() - go <- step + let! hasFirst = e.MoveNextAsync() - if step then - // only create hashset by the time we actually start iterating - use hashSet = new ConcurrentHashSet<_>(CancellationToken.None) - do! hashSet.AddManyAsync itemsToExclude + if hasFirst then + // only create hashset by the time we actually start iterating; + // initialize directly from the seq — taskSeq is sequential so no locking needed. + let hashSet = HashSet<_>(itemsToExclude, HashIdentity.Structural) - while go do + // if true, it was added, and therefore unique, so we return it + // if false, it existed, and therefore a duplicate, and we skip + if hashSet.Add e.Current then + yield e.Current + + while! e.MoveNextAsync() do let current = e.Current - // if true, it was added, and therefore unique, so we return it - // if false, it existed, and therefore a duplicate, and we skip if hashSet.Add current then yield current - let! step = e.MoveNextAsync() - go <- step + } + let distinctUntilChanged (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + taskSeq { + let mutable maybePrevious = ValueNone + + for current in source do + match maybePrevious with + | ValueNone -> + yield current + maybePrevious <- ValueSome current + | ValueSome previous -> + if previous = current then + () // skip + else + yield current + maybePrevious <- ValueSome current } - let exceptOfSeq itemsToExclude (source: TaskSeq<_>) = + let distinctUntilChangedWith (comparer: 'T -> 'T -> bool) (source: TaskSeq<_>) = checkNonNull (nameof source) source - checkNonNull (nameof itemsToExclude) itemsToExclude taskSeq { + let mutable maybePrevious = ValueNone + + for current in source do + match maybePrevious with + | ValueNone -> + yield current + maybePrevious <- ValueSome current + | ValueSome previous -> + if comparer previous current then + () // skip + else + yield current + maybePrevious <- ValueSome current + } + + let distinctUntilChangedWithAsync (comparer: 'T -> 'T -> #Task) (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + taskSeq { + let mutable maybePrevious = ValueNone + + for current in source do + match maybePrevious with + | ValueNone -> + yield current + maybePrevious <- ValueSome current + | ValueSome previous -> + let! areEqual = comparer previous current + + if areEqual then + () // skip + else + yield current + maybePrevious <- ValueSome current + } + + let pairwise (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + taskSeq { + let mutable maybePrevious = ValueNone + + for current in source do + match maybePrevious with + | ValueNone -> maybePrevious <- ValueSome current + | ValueSome previous -> + yield previous, current + maybePrevious <- ValueSome current + } + + let groupBy (projector: ProjectorAction<'T, 'Key, _>) (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + task { use e = source.GetAsyncEnumerator CancellationToken.None - let mutable go = true - let! step = e.MoveNextAsync() - go <- step + let groups = Dictionary<'Key, ResizeArray<'T>>(HashIdentity.Structural) + let order = ResizeArray<'Key>() + + match projector with + | ProjectorAction proj -> + while! e.MoveNextAsync() do + let key = proj e.Current + let mutable ra = Unchecked.defaultof<_> + + if not (groups.TryGetValue(key, &ra)) then + ra <- ResizeArray() + groups[key] <- ra + order.Add key + + ra.Add e.Current + + | AsyncProjectorAction proj -> + while! e.MoveNextAsync() do + let! key = proj e.Current + let mutable ra = Unchecked.defaultof<_> + + if not (groups.TryGetValue(key, &ra)) then + ra <- ResizeArray() + groups[key] <- ra + order.Add key + + ra.Add e.Current + + return + Array.init order.Count (fun i -> + let k = order[i] + k, groups[k].ToArray()) + } - if step then - // only create hashset by the time we actually start iterating - use hashSet = new ConcurrentHashSet<_>(CancellationToken.None) - do hashSet.AddMany itemsToExclude + let countBy (projector: ProjectorAction<'T, 'Key, _>) (source: TaskSeq<_>) = + checkNonNull (nameof source) source - while go do - let current = e.Current + task { + use e = source.GetAsyncEnumerator CancellationToken.None + let counts = Dictionary<'Key, int>(HashIdentity.Structural) + let order = ResizeArray<'Key>() - // if true, it was added, and therefore unique, so we return it - // if false, it existed, and therefore a duplicate, and we skip - if hashSet.Add current then - yield current + match projector with + | ProjectorAction proj -> + while! e.MoveNextAsync() do + let key = proj e.Current + let mutable count = 0 - let! step = e.MoveNextAsync() - go <- step + if not (counts.TryGetValue(key, &count)) then + order.Add key + + counts[key] <- count + 1 + + | AsyncProjectorAction proj -> + while! e.MoveNextAsync() do + let! key = proj e.Current + let mutable count = 0 + + if not (counts.TryGetValue(key, &count)) then + order.Add key + + counts[key] <- count + 1 + + return Array.init order.Count (fun i -> let k = order[i] in k, counts[k]) + } + + let partition (predicate: PredicateAction<'T, _>) (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + task { + use e = source.GetAsyncEnumerator CancellationToken.None + let trueItems = ResizeArray<'T>() + let falseItems = ResizeArray<'T>() + + match predicate with + | Predicate pred -> + while! e.MoveNextAsync() do + let item = e.Current + + if pred item then + trueItems.Add item + else + falseItems.Add item + + | PredicateAsync pred -> + while! e.MoveNextAsync() do + let item = e.Current + let! result = pred item + if result then trueItems.Add item else falseItems.Add item + + return trueItems.ToArray(), falseItems.ToArray() + } + + let chunkBySize chunkSize (source: TaskSeq<'T>) : TaskSeq<'T[]> = + if chunkSize < 1 then + invalidArg (nameof chunkSize) $"The value must be positive, but was %i{chunkSize}." + + checkNonNull (nameof source) source + + taskSeq { + // Use a fixed-size array with a count index to avoid ResizeArray overhead. + let buffer = Array.zeroCreate<'T> chunkSize + let mutable count = 0 + + for item in source do + buffer.[count] <- item + count <- count + 1 + + if count = chunkSize then + yield Array.copy buffer + count <- 0 + + if count > 0 then + // Last partial chunk: copy only the filled portion. + yield buffer.[0 .. count - 1] + } + + let chunkBy (projection: 'T -> 'Key) (source: TaskSeq<'T>) : TaskSeq<'Key * 'T[]> = + checkNonNull (nameof source) source + + taskSeq { + let mutable maybeCurrentKey = ValueNone + let mutable currentChunk = ResizeArray<'T>() + + for item in source do + let key = projection item + + match maybeCurrentKey with + | ValueNone -> + maybeCurrentKey <- ValueSome key + currentChunk.Add item + | ValueSome prevKey -> + if prevKey = key then + currentChunk.Add item + else + yield prevKey, currentChunk.ToArray() + currentChunk.Clear() // reuse backing array; ToArray() already captured a snapshot + currentChunk.Add item + maybeCurrentKey <- ValueSome key + + match maybeCurrentKey with + | ValueNone -> () + | ValueSome lastKey -> yield lastKey, currentChunk.ToArray() + } + + let chunkByAsync (projection: 'T -> #Task<'Key>) (source: TaskSeq<'T>) : TaskSeq<'Key * 'T[]> = + checkNonNull (nameof source) source + + taskSeq { + let mutable maybeCurrentKey = ValueNone + let mutable currentChunk = ResizeArray<'T>() + + for item in source do + let! key = projection item + + match maybeCurrentKey with + | ValueNone -> + maybeCurrentKey <- ValueSome key + currentChunk.Add item + | ValueSome prevKey -> + if prevKey = key then + currentChunk.Add item + else + yield prevKey, currentChunk.ToArray() + currentChunk.Clear() // reuse backing array; ToArray() already captured a snapshot + currentChunk.Add item + maybeCurrentKey <- ValueSome key + + match maybeCurrentKey with + | ValueNone -> () + | ValueSome lastKey -> yield lastKey, currentChunk.ToArray() + } + + let windowed windowSize (source: TaskSeq<_>) = + if windowSize <= 0 then + invalidArg (nameof windowSize) $"The value must be positive, but was %i{windowSize}." + + checkNonNull (nameof source) source + + taskSeq { + // Ring buffer: arr holds elements in circular order. + // 'count' tracks total elements seen; count % windowSize is the next write position. + let arr = Array.zeroCreate windowSize + let mutable count = 0 + + for item in source do + arr.[count % windowSize] <- item + count <- count + 1 + + if count >= windowSize then + // Copy ring buffer in source order into a fresh array. + let result = Array.zeroCreate windowSize + let start = count % windowSize // index of oldest element in the ring + + if start = 0 then + Array.blit arr 0 result 0 windowSize + else + Array.blit arr start result 0 (windowSize - start) + Array.blit arr 0 result (windowSize - start) start + yield result } diff --git a/src/FSharp.Control.TaskSeq/Utils.fs b/src/FSharp.Control.TaskSeq/Utils.fs index c02bab35..147734bf 100644 --- a/src/FSharp.Control.TaskSeq/Utils.fs +++ b/src/FSharp.Control.TaskSeq/Utils.fs @@ -1,22 +1,15 @@ namespace FSharp.Control -open System.Threading.Tasks open System -open System.Diagnostics -open System.Threading +open System.Threading.Tasks [] module ValueTaskExtensions = - /// Extensions for ValueTask that are not available in NetStandard 2.1, but are - /// available in .NET 5+. We put them in Extension space to mimic the behavior of NetStandard 2.1 type ValueTask with - - /// (Extension member) Gets a task that has already completed successfully. static member inline CompletedTask = - // This mimics how it is done in .NET itself + // This mimics how it is done in net5.0 and later internally Unchecked.defaultof - module ValueTask = let False = ValueTask() let True = ValueTask true @@ -24,15 +17,15 @@ module ValueTask = let inline ofSource taskSource version = ValueTask(taskSource, version) let inline ofTask (task: Task<'T>) = ValueTask<'T> task - let inline ignore (vtask: ValueTask<'T>) = + let inline ignore (valueTask: ValueTask<'T>) = // this implementation follows Stephen Toub's advice, see: // https://github.com/dotnet/runtime/issues/31503#issuecomment-554415966 - if vtask.IsCompletedSuccessfully then + if valueTask.IsCompletedSuccessfully then // ensure any side effect executes - vtask.Result |> ignore + valueTask.Result |> ignore ValueTask() else - ValueTask(vtask.AsTask()) + ValueTask(valueTask.AsTask()) [] let inline FromResult (value: 'T) = ValueTask<'T> value @@ -40,7 +33,6 @@ module ValueTask = [] let inline ofIValueTaskSource taskSource version = ofSource taskSource version - module Task = let inline fromResult (value: 'U) : Task<'U> = Task.FromResult value let inline ofAsync (async: Async<'T>) = task { return! async } @@ -72,14 +64,15 @@ module Async = let inline ofTask (task: Task<'T>) = Async.AwaitTask task let inline ofUnitTask (task: Task) = Async.AwaitTask task let inline toTask (async: Async<'T>) = task { return! async } - let inline bind binder (task: Async<'T>) : Async<'U> = ExtraTopLevelOperators.async { return! binder task } - let inline ignore (async': Async<'T>) = async { - let! _ = async' - return () - } + let inline ignore (async: Async<'T>) = Async.Ignore async let inline map mapper (async: Async<'T>) : Async<'U> = ExtraTopLevelOperators.async { let! result = async return mapper result } + + let inline bind binder (async: Async<'T>) : Async<'U> = ExtraTopLevelOperators.async { + let! result = async + return! binder result + } diff --git a/src/FSharp.Control.TaskSeq/Utils.fsi b/src/FSharp.Control.TaskSeq/Utils.fsi index d34a1e57..d252d45a 100644 --- a/src/FSharp.Control.TaskSeq/Utils.fsi +++ b/src/FSharp.Control.TaskSeq/Utils.fsi @@ -6,10 +6,12 @@ open System.Threading.Tasks.Sources [] module ValueTaskExtensions = - type System.Threading.Tasks.ValueTask with - /// (Extension member) Gets a task that has already completed successfully. - static member inline CompletedTask: System.Threading.Tasks.ValueTask + /// Shims back-filling .NET 5+ functionality for use on netstandard2.1 + type ValueTask with + + /// (Extension member) Gets a ValueTask that has already completed successfully. + static member inline CompletedTask: ValueTask module ValueTask = @@ -24,13 +26,13 @@ module ValueTask = /// /// The function is deprecated since version 0.4.0, - /// please use in its stead. See . + /// please use in its stead. See . /// [] val inline FromResult: value: 'T -> ValueTask<'T> /// - /// Initialized a new instance of with an representing + /// Initializes a new instance of with an /// representing its operation. /// val inline ofSource: taskSource: IValueTaskSource -> version: int16 -> ValueTask @@ -42,21 +44,24 @@ module ValueTask = [] val inline ofIValueTaskSource: taskSource: IValueTaskSource -> version: int16 -> ValueTask - /// Creates a ValueTask form a Task<'T> + /// Creates a ValueTask from a Task<'T> val inline ofTask: task: Task<'T> -> ValueTask<'T> - /// Ignore a ValueTask<'T>, returns a non-generic ValueTask. - val inline ignore: vtask: ValueTask<'T> -> ValueTask + /// Convert a ValueTask<'T> into a non-generic ValueTask, ignoring the result + val inline ignore: valueTask: ValueTask<'T> -> ValueTask module Task = - /// Convert an Async<'T> into a Task<'T> + /// Creates a Task<'U> that's completed successfully with the specified result. + val inline fromResult: value: 'U -> Task<'U> + + /// Starts the `Async<'T>` computation, returning the associated `Task<'T>` val inline ofAsync: async: Async<'T> -> Task<'T> - /// Convert a unit-task into a Task + /// Convert a non-generic Task into a Task val inline ofTask: task': Task -> Task - /// Convert a non-task function into a task-returning function + /// Convert a plain function into a task-returning function val inline apply: func: ('a -> 'b) -> ('a -> Task<'b>) /// Convert a Task<'T> into an Async<'T> @@ -66,8 +71,8 @@ module Task = val inline toValueTask: task: Task<'T> -> ValueTask<'T> /// - /// Convert a ValueTask<'T> to a Task<'T>. To use a non-generic ValueTask, - /// consider using: . + /// Convert a ValueTask<'T> to a Task<'T>. For a non-generic ValueTask, + /// consider: . /// val inline ofValueTask: valueTask: ValueTask<'T> -> Task<'T> @@ -80,25 +85,22 @@ module Task = /// Bind a Task<'T> val inline bind: binder: ('T -> #Task<'U>) -> task: Task<'T> -> Task<'U> - /// Create a task from a value - val inline fromResult: value: 'U -> Task<'U> - module Async = /// Convert an Task<'T> into an Async<'T> val inline ofTask: task: Task<'T> -> Async<'T> - /// Convert a unit-task into an Async + /// Convert a non-generic Task into an Async val inline ofUnitTask: task: Task -> Async - /// Convert a Task<'T> into an Async<'T> + /// Starts the `Async<'T>` computation, returning the associated `Task<'T>` val inline toTask: async: Async<'T> -> Task<'T> /// Convert an Async<'T> into an Async, ignoring the result - val inline ignore: async': Async<'T> -> Async + val inline ignore: async: Async<'T> -> Async /// Map an Async<'T> val inline map: mapper: ('T -> 'U) -> async: Async<'T> -> Async<'U> /// Bind an Async<'T> - val inline bind: binder: (Async<'T> -> Async<'U>) -> task: Async<'T> -> Async<'U> + val inline bind: binder: ('T -> Async<'U>) -> async: Async<'T> -> Async<'U>